更高效的同步鎖-GCD 同步鎖

本文整理自《Effective Objective-C 2.0》,通過分析比較不同的同步鎖的優缺點,使用GCD方法一步步找到更高效的同步鎖。

在Objective-C中,如果有多個線程要執行同一份代碼,那么這時就會出現線程安全問題。首先,我們看下什么時候線程安全問題。

線程安全

如果一段代碼所在的進程中有多個線程在同時運行,那么這些線程就有可能會同時運行這段代碼。假如多個線程每次運行結果和單線程運行的結果是一樣的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。

由于可讀寫的全局變量及靜態變量(在 Objective-C 中還包括屬性和實例變量)可以在不同線程修改,所以這兩者也通常是引起線程安全問題的所在。

Objective-C中的同步鎖

在 Objective-C 中,如果有多個線程執行同一份代碼,那么有可能會出現線程安全問題。這種情況下,就需要使用所來實現某種同步機制。

在 GCD出現之前,有兩種方法,一種采用的是內置的“同步塊”(synchronization block),另一種方法是使用鎖對象。

同步塊(synchronization block)
- (void)synchronizedMethod {
    @synchronized (self) {
        //Safe
    }
}

這種寫法會根據給定的對象,自動創建一個鎖,并等待塊中的代碼執行完畢。執行到這段代碼結尾處,鎖就釋放了。

該同步方法的優點就是我們不需要在代碼中顯式的創建鎖對象,便可以實現鎖的機制。然而,濫用@synchronized (self)則會降低代碼效率,因為公用同一個鎖的那些同步塊,都必須按順序執行。若是在self對象上頻繁加鎖,程序可能要等另一段與此無關的代碼執行完畢,才能繼續執行當前代碼,這樣效率就低了。
注:因為@synchronized (self)方法針對self只有一個鎖,相當于對于self的所有用到同步塊的地方都是公用同一個鎖,所以如果有多個同步塊,則其他的同步塊都要等待當前同步塊執行完畢才能繼續執行。

- (void)synchronizedAMethod {
    @synchronized (self) {
        //Safe
    }
}

- (void)synchronizedBMethod {
    @synchronized (self) {
        //Safe
    }
}

- (void)synchronizedCMethod {
    @synchronized (self) {
        //Safe
    }
}

以上代碼,如果當前synchronizedAMethod方法正在執行,則synchronizedBMethodsynchronizedCMethod方法需要等待synchronizedAMethod完畢后才能執行,不能達到并發的效果。

鎖對象

@property (nonatomic,strong) NSLock *lock;

_lock = [[NSLock alloc] init];

- (void)synchronizedMethod {
    [_lock lock];
    //Safe
    [_lock unlock];
}

以上是簡單鎖對象的實現方式,但是如果鎖使用不當,會出現死鎖現象,這時可以使用NSRecursiveLock這種“遞歸鎖”(recursive lock)。

除了以上鎖對象,還有NSConditionLock 條件鎖 、NSDistributedLock 分布式鎖 ,這些適用于不同的場景,這里就不展開說了。

以上這些鎖,使用的時候還是有缺陷的。在極端情況下,同步塊會導致死鎖,另外,效率也不見得高,而如果直接使用鎖對象的話,一旦遇到死鎖,就會非常麻煩。

GCD鎖

在開始說GCD鎖之前,我們先了解一下GCD的中的任務派發和隊列。

任務派發
任務派發方式 說明
dispatch_sync() 同步執行,完成了它預定的任務后才返回,阻塞當前線程
dispatch_async() 異步執行,會立即返回,預定的任務會完成但不會等它完成,不阻塞當前線程
隊列種類
隊列種類 說明
串行隊列 每次只能執行一個任務,并且必須等待前一個執行任務完成
并發隊列 一次可以并發執行多個任務,不必等待執行中的任務完成
GCD隊列種類
GCD隊列種類 獲取方法 隊列類型 說明
主隊列 dispatch_get_main_queue 串行隊列 主線中執行
全局隊列 dispatch_get_global_queue 并發隊列 子線程中執行
用戶隊列 dispatch_queue_create 串并都可以 子線程中執行
以前同步鎖的實現方式

在Objective-C中,屬性就是開發者經常需要同步的地方。通常開發者想省事的話(以前我也是這樣覺得),會這樣寫:

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

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

以上代碼除了上文提到的效率低以外,還有一個問題,就是該方法并不能保證訪問該對象時絕對是線程安全的。雖然,這種方法在訪問屬性時,確實是“原子”的,也必定能從中獲取到有效值,然而在同一線程上多次調用getter方法,每次獲取到的結果未必相同。在兩次訪問操作之間,其他線程可能會寫入新的屬性值。此時,只能保證讀寫操作是“原子”的,而多個線程的執行順序,我們沒有辦法控制。

使用GCD串行隊列來實現同步鎖

有種簡單而高效的方法可以替代同步塊或鎖對象,那就是使用“串行同步隊列”。將讀取操作以及寫入操作都安排在同一個隊列里,即可保證數據同步。
用法如下:

@property (nonatomic,strong) dispatch_queue_t syncQueue;

_syncQueue = dispatch_queue_create("com.effetiveobjectivec.syncQueue", NULL);

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

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

此模式的思路是:把設置操作與獲取操作都安排在序列化的隊列里執行,這樣的話,所有針對屬性的訪問操作就都同步了。
注:getter方法中,用一個臨時變量來保存值,是因為在block中return的話,只是return到block中了,沒有真正返回到對應的getter方法中,而__block是為了可以在block中改變改臨時變量而用。

雖然問題解決了,但是我們還可以進一步優化。設置方法不一定非得是同步的。設置實例變量所用的塊,并不需要向設置方法返回什么值。那代碼可以改成:

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

這次把同步改成了異步,也許看來,這樣改動,性能是會有提升的,但是你測一下程序的性能,可能會發現這種寫法比原來慢。因為執行異步派發時,是需要拷貝塊。若拷貝塊所用的時間明顯超過執行塊所需的時間,則這種做法將比原來的更慢。
注:本例子代碼比較簡單,若是要執行的塊代碼邏輯比較復雜的話,那么該寫法可能還是比原來的塊些

使用GCD并發隊列來實現同步鎖

對于屬性的讀寫,我們希望多個獲取方法可以并發執行,而獲取方法與設置方法之間不能并發執行,利用這個特點,還能寫出更快一些的代碼來。此時正可體現出GCD的好處。而用同步鎖或鎖對象,是無法輕易實現下面這種方案的。這次我們使用并發隊列:

@property (nonatomic,strong) dispatch_queue_t syncQueue;

_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

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

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

以上代碼,還無法正確實現同步。因為所有讀寫操作都會在同一個隊列上執行,而該隊列是并發隊列,所有讀取和寫入操作都可以隨時執行,沒有達到同步效果。此問題我們可以通過一個簡單的GCD功能解決--柵欄(barrier)。下列函數可以向隊列中派發塊,將其作為柵欄使用:

void dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);
void dispatch_barrier_sync(dispatch_queue_t queue, dispatch_block_t block);

注:dispatch_barrier_async如果傳入自己創建的并行隊列時,會阻塞當前隊列執行,而不阻塞當前線程。
dispatch_barrier_sync如果傳入自己創建的并行隊列時,阻塞當前隊列的同時也會阻塞當前線程,請注意

并發隊列如果發現接下來要處理的塊是個柵欄塊,那么就一直要等當前所有并發塊都執行完畢,才會單獨執行這個柵欄塊。這待柵欄塊執行完畢,在按正常方式繼續向下處理。這樣就解決了并發隊列的同步問題。

GCD并發隊列中加入柵欄

本例中,可以用柵欄塊來實現屬性的設置方法。在設置方法中使用了柵欄塊之后,對屬性的讀取操作依然可以并發執行,但寫入操作卻必須單獨執行了。在下圖中演示的這個隊列中,有多個讀取操作,而且還有一個寫入操作。

在這個并發隊列中,讀取操作是用普通的塊來實現的,而寫入操作則是用柵欄塊來實現的 讀取操作可以并行,但寫入操作必須單獨執行,因為它是柵欄塊

實現代碼很簡單:

@property (nonatomic,strong) dispatch_queue_t syncQueue;

//_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
/* 這里應該使用自己創建的并發隊列,因為蘋果文檔中指出,如果使用的是全局隊列或者創建的不是并發隊列,
則dispatch_barrier_async實際上就相當于dispatch_async,就達不到我們想要的效果了 */
_syncQueue = dispatch_queue_create("com.effetiveobjectivec.syncQueue", DISPATCH_QUEUE_CONCURRENT);

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

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

測試一下性能,你就會發現,這種做法肯定比使用串行隊列要快。其中,設置函數也可以改用同步柵欄塊來實現,那樣做可能會更高效,其原因之前已經解釋過了——這里就要權衡拷貝塊的時間和塊執行時間了

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,428評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,024評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,285評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,548評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,328評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,878評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,971評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,098評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,616評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,554評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,725評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,243評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,971評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,361評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,613評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,339評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,695評論 2 370

推薦閱讀更多精彩內容