GCD之同步鎖和派發隊列

        在OC中,若是有多個線程要執行同一份代碼,那麼就可能會出現問題.(好比出現讀寫不一致的狀況)這種狀況下一般須要使用鎖來實現某種同步機制.在GCD 出現以前,有兩種辦法,第一種是使用內置的同步塊 synchronization block安全

- (void)synchronizedMethod {
    @synchronized(self) {
        //
    }
}

這種寫法會根據給定的對象自動建立一個鎖,並等到塊中的代碼執行完畢.執行到代碼結尾處,鎖就會自動釋放. 併發

 須要注意的是:濫用@synchronized(self)會下降代碼的執行效率,由於公用一個鎖的那些同步塊必須按照順序執行.如果在self上頻繁加鎖,那麼程序可能要等待另外一段無關的代碼執行完畢才能繼續執行當前的代碼.異步

另外一方法是直接使用 NSLock對象.async

_lock = [[NSLock alloc] init];

- (void)synchronizedMethod_lock {
    [_lock lock];
    // safe
    [_lock unlock];
}

    這兩種方法都很好,可是也有缺陷.好比:極端狀況下,同步塊會致使死鎖,並且性能也不見得高效,而若是直接使用鎖對象的話,一旦趕上死鎖將會很是難處理.函數

    替代方案就是使用GCD,他能以更簡單 更高效的形式爲代碼加鎖.  好比說屬性就是開發者常常須要同步的地方,這種屬性須要作成原子的 . 用atomic來修飾屬性就能夠實現這一點 .若是開發者想本身來編寫訪問方法的話,那麼能夠這樣寫:性能

- (NSString *)someString {
    @synchronized(self) {
        return _someString;
    }
}

- (void)setSomeString:(NSString *)someString {
    @synchronized(self) {
        _someString = someString;
    }
}

    剛纔說過,濫用@synchronized(self) 會很危險,應爲全部的同步塊都會彼此搶奪同一個鎖.要是有不少屬性都這麼寫的話,那麼每一個屬性的同步塊都要等到其餘全部同步塊執行完畢才能執行,這並非咱們想要的效果.咱們只是想令每一個屬性各自獨立的同步.另外,這麼作只能提供必定程度上的線程安全,並不能保證絕對的線程安全. 固然,  訪問屬性的操做的確都是原子的. 使屬性時,一定能從中獲取到有效值,而後在同一個線程上屢次調用getter方法,每次獲取的結果卻未必相同,在兩次訪問操做之間,其餘線程可能會寫入新的屬性值.測試

        有種簡單而高效的辦法能夠代替同步塊或者鎖對象,那就是使用 "串行同步隊列" .將要讀取和寫入的操做安排在一個同步隊列裏,便可保證數據同步.優化

用法以下:atom

dispatch_queue_t _syncQueue;
_syncQueue = dispathc_queue_create("syncQueue",NULL);
- (NSString *)someString {
    __block NSString *temp;
    dispatch_sync(_syncQueue, ^{
        temp = _otherString;
    });
    return temp;
}

- (void)setSomeString:(NSString *)someString {
    dispatch_barrier_sync(_syncQueue, ^{
        _someString = someString;
    });
}

        此模式的思路是:把setter和getter都安排在同步隊列裏執行,如此一來,全部針對屬性的訪問操做就同步了. 全部加鎖任務都在GCD中處理,而GCD是在至關深的底層實現的,相對於剛纔提到的兩種,GCD的效率會更高.所以咱們無需擔憂加鎖的事,只需把訪問方法寫好就好了.spa

        而後這還能夠進一步進行優化, setter方法不必定非得是同步的 設置實例變量所用的塊並無返回值,因此setter方法能夠改爲下面這樣:

- (void)setSomeString:(NSString *)someString {
    dispatch_async(_syncQueue, ^{
        _someString = someString;
    });
}

        此次只是把同步派發改爲了異步派發,從調用者的角度來看,這個小改動能夠提高設置方法執行速度,而讀取操做與寫入操做依然會按順序執行. 可是這麼改回帶來另一個問題:若是你測試一下性能,可能會發現這種寫法比原來慢,應爲異步執行派發是,須要拷貝塊.若拷貝塊所用到的時間明顯超過執行塊所用到的時間,則這種作法將比原來慢. 因爲這裏的例子比較簡單,因此這麼改完以後可能會變慢. 如果派發給隊列的塊要執行的任務比較繁重時,那麼仍然能夠考慮這種備選方案.

    多個獲取方法可併發執行,而getter方法和setter方法不能併發執行,利用這個特色能夠寫出更高效的代碼. ---將setter方法和getter方法都放到同一個併發隊列中,因爲是併發隊列,因此讀寫操做能夠隨時執行,而後咱們並不但願這樣. 這個問題用一個簡單的GCD功能就能夠解決,他就是柵欄.下面的函數能夠向隊列中派發塊,將其做爲柵欄使用

dispatch_barrier_async(dispatch_queue_t queue, ^{
        
    });
    dispatch_barrier_sync(dispatch_queue_t queue, ^{
        
    });

        在隊列裏,柵欄塊必須單獨執行,不能與其餘塊併發執行.這隻對併發隊列有意義,由於串行隊列中的塊老是按照順序執行的. 併發隊列若是發現接下來要執行的塊是柵欄快,那麼就要一直等當前全部的併發快都執行完畢,纔會單獨的執行這個柵欄塊.等到柵欄快執行事後,在按照正常的方式繼續向下執行.

        在下面的例子中 用柵欄快實現屬性的設置方法.在setter方法中使用了柵欄塊之後,對屬性的讀取操做依然能夠併發執行,可是寫入操做卻必須單獨執行了.

_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

- (NSString *)someString {
    __block NSString *temp;
    dispatch_sync(_syncQueue, ^{
        temp = _otherString;
    });
    return temp;
}

- (void)setSomeString:(NSString *)someString {
    dispatch_barrier_async(_syncQueue, ^{
        _someString = someString;
    });
}

   其執行示意圖以下:

          |  讀取 讀取 讀取

          |  讀取 讀取

          |  寫入操做(柵欄塊)

          |  讀取 讀取

          |  寫入操做(柵欄塊)

          |  讀取 讀取 讀取

          ⬇️  讀取 讀取

 在這個併發隊列中,讀取操做使用普通的快來實現,而寫入操做則是用柵欄塊來實現的,讀取操做能夠並行,但寫入操做必須單獨執行,由於他是柵欄塊.

 這種作法比使用串行隊列要塊.注意:設置函數也能夠改用同步的柵欄塊來實現,這樣的話效率可能會更高,其緣由剛纔已經解釋過了.固然最好仍是根據實際狀況選擇最合適的方案.

    -->派發隊列可用來表示同步語義,這用作法要比使用 @synchronized塊或者NSLock對象更簡單

    -->將同步和異步結合起來,能夠實現與普通加鎖機制同樣的同步行爲,而這麼作卻不會阻塞 執行異步派發的線程.

    -->使用同步隊列及柵欄塊,能夠令同步行爲更加高效.

 

參考書籍--<編寫高質量iOS和OSX代碼的52個有效方法>

相關文章
相關標籤/搜索