作者:Ryan Kaplan 譯者:徐嘉宏
如果你曾經(jīng)使用Objective-C做過并發(fā)編程痊班,那你肯定見過@synchronized這個結(jié)構(gòu)碳褒。@synchronized這個結(jié)構(gòu)發(fā)揮了和鎖一樣的作用:它避免了多個線程同時執(zhí)行同一段代碼哥攘。和使用NSLock進(jìn)行創(chuàng)建鎖、加鎖腰耙、解鎖相比,在某些情況下@synchronized會更方便迂猴、更易讀滓技。
如果你從來沒有使用過@synchronized,具體如何使用可以參考下面的實例冯吓。本文的將圍繞我對@synchronized的原理的探究進(jìn)行講述倘待。
使用@synchronized的例子
假如要用Objective-C實現(xiàn)一個線程安全的隊列疮跑,我們大概會這樣寫:
@implementation ThreadSafeQueue {
NSMutableArray *_elements;
NSLock *_lock;
}
- (instancetype)init {
self = [super init];
if (self) {
_elements = [NSMutableArray array];
_lock = [[NSLock alloc] init];
}
return self;
}
- (void)push:(id)element {
[_lock lock];
[_elements addObject:element];
[_lock unlock];
}
@end
ThreadSafeQueue這個類首先有一個init方法组贺,這里初始化了兩個變量:一個_elements數(shù)組和一個NSLock。另外祖娘,有一個需要獲取這個鎖以插入元素到數(shù)組中然后釋放鎖的push:方法失尖。許多線程會同時調(diào)用push:方法啊奄,然而[ _elements addObject:element];
這行代碼也只能同時被一條線程訪問。這個流程應(yīng)該是這樣的:
- 線程A調(diào)用push:方法
- 線程B調(diào)用push:方法
- 線程B調(diào)用
[_lock lock]
掀潮,因為沒有其他線程持有這個鎖菇夸,因此線程B取得了這個鎖 - 線程A調(diào)用
[_lock lock]
,但是此時這個鎖被線程B所持有仪吧,所以這個方法調(diào)用并沒有返回庄新,使線程A暫停了執(zhí)行 - 線程B添加了一個元素到_elements中,然后調(diào)用
[ _lock unlock]
方法薯鼠。此時择诈,線程A的[ _lock unlock]
方法返回了,接著繼續(xù)執(zhí)行線程A的元素插入操作
使用@synchronized出皇,我們可以更簡潔明了的實現(xiàn)剛才的功能:
@implementation ThreadSafeQueue {
NSMutableArray *_elements;
}
- (instancetype)init {
self = [super init];
if (self) {
_elements = [NSMutableArray array];
}
return self;
}
- (void)increment {
@synchronized (self) {
[_elements addObject:element];
}
}
@end
這個@synchronized的代碼塊和前面例子中的[ _lock unlock]
羞芍、[ _lock unlock]
的作用相同作用效果。你可以把它理解成把self當(dāng)作一個NSLock來對self進(jìn)行加鎖郊艘。在運行{后的代碼前獲取鎖荷科,并在運行}后的其他代碼前釋放這個鎖。這非常的方便纱注,因為這意味著你永遠(yuǎn)不會忘了調(diào)用unlock
你也可以在任何Objective-C的對象上使用@synchronized畏浆。因此,同樣的我們也可以像下面的例子里一樣奈附,使用@synchronized(_elements)
來代替@synchronized(self)
全度,這兩者的效果是一致的。
回到我的探究上來
我對@synchronized的實現(xiàn)很好奇斥滤,于是我在谷歌搜索了它的一些細(xì)節(jié)将鸵。我找到了關(guān)于這個的一些回答 @synchronized是如何加鎖/解鎖的 在@synchronized中改變加鎖的對象 Apple的文檔,但沒有一個答案能給我足夠深入的解釋佑颇。傳入@synchronized的參數(shù)和這個鎖有什么關(guān)系顶掉?@synchronized是否持有它所加鎖的對象?如果傳入@synchronized代碼塊的對象在代碼塊里被析構(gòu)了或者被置為nil了會怎么樣挑胸?這些都是我想問的問題痒筒。在下文中,我會分享我的發(fā)現(xiàn)茬贵。
關(guān)于@synchronized的Apple的文檔中提到簿透,@synchronized代碼塊隱式地給被保護(hù)的代碼段添加了一個異常處理塊。這就是為什么在給某個對象保持同步的時候解藻,如果拋出了異常老充,鎖就會被釋放。
在 stackoverflow的一個回答中提到螟左,@synchronized塊會轉(zhuǎn)化成一對objc_sync_enter
和objc_sync_exit
的函數(shù)調(diào)用啡浊。我們并不知道這些函數(shù)都干了什么觅够,但是根據(jù)這個我們可以推斷,編譯器會像這樣轉(zhuǎn)化代碼:
@synchronized(obj) {
// do work
}
轉(zhuǎn)換成大概像這樣的:
@try {
objc_sync_enter(obj);
// do work
} @finally {
objc_sync_exit(obj);
}
具體什么是objc_sync_enter
和object_sync_exit
以及它們是如何實現(xiàn)的巷嚣,我們通過Command+點擊這兩個函數(shù)跳轉(zhuǎn)到了<objc/objc-sync.h>
里喘先,這里有我們要找的兩個函數(shù):
// Begin synchronizing on 'obj'.
// Allocates recursive pthread_mutex associated with 'obj' if needed.
int objc_sync_enter(id obj)
// End synchronizing on 'obj'.
int objc_sync_exit(id obj)
在文件的最后,有一個蘋果工程師也是人的提示;)
// The wait/notify functions have never worked correctly and no longer exist.
int objc_sync_wait(id obj, long long milliSecondsMaxWait);
int objc_sync_notify(id obj);
總之廷粒,關(guān)于objc_sync_enter
的文檔告訴了我們:@synchronized是基于一個遞歸鎖[1] 來傳遞一個對象的窘拯。什么時候分配內(nèi)存、如何分配內(nèi)存的坝茎?如何處理nil值树枫?幸運的是,Objective-C運行時是開源的景东,所以我們可以閱讀它的源碼找到答案砂轻。
你可以在這里查看所有objc-sync
的源碼,但是我會領(lǐng)你在更高的層面通讀這些源碼斤吐。我們先從文件頂部的數(shù)據(jù)結(jié)構(gòu)看起搔涝。我會為你解釋下面的源碼因此你不必花時間來嘗試解讀這些代碼。
typedef struct SyncData {
id object;
recursive_mutex_t mutex;
struct SyncData* nextData;
int threadCount;
} SyncData;
typedef struct SyncList {
SyncData *data;
spinlock_t lock;
} SyncList;
static SyncList sDataLists[16];
首先和措,我們看到了結(jié)構(gòu)體struct SyncData
的定義庄呈。這個結(jié)構(gòu)體包含了一個object
(傳入@synchronized
的對象)還有一個關(guān)聯(lián)著這個鎖以及被鎖對象的recursive_mutex_t
。每個SyncData
含有一個指向其他SyncData
的指針nextData
派阱,因此你可以認(rèn)為每個SyncData
都是鏈表里的一個節(jié)點诬留。最后,每個SyncData
含有一個threadCount
來表示在使用或者等待鎖的線程的數(shù)量贫母。這很有用文兑,因為SyncData
是被緩存的,當(dāng)threadCount == 0
時腺劣,表示一個SyncData
的實例能被復(fù)用绿贞。
接著,我們有了struct SyncList
的定義橘原。正如我在前文中所提到的籍铁,你可以把一個SyncData
當(dāng)作鏈表中的一個節(jié)點。每個SyncList
結(jié)構(gòu)都有一個指向SyncData
鏈表頭部的指針趾断,就像一個用于避免多線程并發(fā)的修改該鏈表的鎖一樣拒名。
這個代碼塊的最后一行之上是一個sDataLists
的定義,這是一個SyncList結(jié)構(gòu)的數(shù)組芋酌。剛開始可能看起來不太像增显,但這個sDataList
數(shù)組是一個哈希表(類似NSDictionary
),用于把Objectice-C對象映射到他們對應(yīng)的鎖隔嫡。
當(dāng)你調(diào)用objc_sync_enter(obj)
的時候甸怕,它通過一個記錄obj
地址的哈希表來找到對應(yīng)的SyncData
,然后對其加鎖腮恩。當(dāng)你調(diào)用objc_sync_exit
的時候梢杭,它以同樣的方式找到對應(yīng)的SyncData
并將其解鎖。
很好秸滴!現(xiàn)在我們知道了@synchronized
是如何關(guān)聯(lián)一個鎖和那個被加同步鎖的對象武契,接下來,我會講講當(dāng)一個對象在@synchronized
代碼塊中被析構(gòu)或者被置nil會發(fā)生什么荡含。
如果你看源碼的話咒唆,你會發(fā)現(xiàn)objc_sync_enter
里面并沒有retains
或者release
。因此释液,它并不會持有傳入的對象全释,或者也有可能是因為它是在arc中編譯的。我們可以通過以下的代碼來進(jìn)行測試:
NSDate *test = [NSDate date];
// This should always be `1`
NSLog(@"%@", @([test retainCount]));
@synchronized (test) {
// This will be `2` if `@synchronized` somehow
// retains `test`
NSLog(@"%@", @([test retainCount]));
}
對于每個的持有數(shù)误债,輸出總為1浸船。因此objc_sync_enter
不會持有傳入的對象。這很有意思寝蹈。如果你需要同步的對象唄析構(gòu)了李命,然后可能另外一個新的對象被分配到了這個內(nèi)存地址上,很可能其他線程正嘗試同步那個有著和原對象有著相同地址的新的對象箫老。在這種情況下封字,其他線程會被阻塞直到當(dāng)前線程完成了自己的同步代碼塊。這似乎沒什么毛病耍鬓。這聽起來像這種實現(xiàn)是已被知曉的而且也沒什么問題阔籽。我并沒有看到其他更好的替代方案。
那如果這個對象在@synchronized
代碼塊中被設(shè)成nil會怎樣呢牲蜀?再來看看我們的實現(xiàn):
NSString *test = @"test";
@try {
// Allocates a lock for test and locks it
objc_sync_enter(test);
test = nil;
} @finally {
// Passed `nil`, so the lock allocated in `objc_sync_enter`
// above is never unlocked or deallocated
objc_sync_exit(test);
}
調(diào)用objc_sync_enter
的時候傳入test
仿耽,調(diào)用objc_sync_exit
的時候傳入nil
。若objc_sync_exit
傳入nil
的時候什么都不做各薇,那么也不再會有人去釋放這個鎖项贺。這很糟糕。
Objective-C會那么輕易的被這種問題影響嗎峭判?下面的代碼把一個會被置nil的指針傳入@synchronized
开缎。然后在后臺線程中往@synchronized
中傳入一個指向同一對象的指針。如果在@synchronized
中把一個對象置為nil讓這個鎖處于加鎖的狀態(tài)林螃,那么在第二個@synchronized
中的代碼將永遠(yuǎn)不會被運行奕删。在控制臺中我們應(yīng)該什么都看不到。
NSNumber *number = @(1);
NSNumber *thisPtrWillGoToNil = number;
@synchronized (thisPtrWillGoToNil) {
/**
* Here we set the thing that we're synchronizing on to `nil`. If
* implemented naively, the object would be passed to `objc_sync_enter`
* and `nil` would be passed to `objc_sync_exit`, causing a lock to
* never be released.
*/
thisPtrWillGoToNil = nil;
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^ {
NSCAssert(![NSThread isMainThread], @"Must be run on background thread");
/**
* If, as mentioned in the comment above, the synchronized lock is never
* released, then we expect to wait forever below as we try to acquire
* the lock associated with `number`.
*
* This doesn't happen, so we conclude that `@synchronized` must deal
* with this correctly.
*/
@synchronized (number) {
NSLog(@"This line does indeed get printed to stdout");
}
});
當(dāng)我們運行上述代碼時疗认,這行代碼卻的確被打印到控制臺上了完残!因此可以證明伏钠,Objective-C能很好的處理這種情況。我打賭這種情況是被編譯器處理過的谨设,大概如下:
NSString *test = @"test";
id synchronizeTarget = (id)test;
@try {
objc_sync_enter(synchronizeTarget);
test = nil;
} @finally {
objc_sync_exit(synchronizeTarget);
}
有了這種實現(xiàn)熟掂,傳入objc_sync_enter
和objc_sync_exit
的對象總是相同的。當(dāng)傳入nil
的時候他們什么都不會做扎拣。這引出了一個很棘手的debug場景:如果你往@synchronized
里傳入nil
赴肚,那么相當(dāng)于你并沒有進(jìn)行過加鎖操作,同時你的代碼將不再是線程安全的了二蓝!如果你被莫名其妙的問題困擾著誉券,那么先確保你沒有把nil
傳入你的@synchronized
代碼塊。你可以通過給objc_sync_nil
設(shè)置一個符號斷點來檢查刊愚,objc_sync_nil
是一個空方法踊跟,會在往objc_sync_enter
傳入nil的時候調(diào)用,這會讓調(diào)試方便的多鸥诽。
現(xiàn)在琴锭,我的問題得到了回答。
- 對于每個加了同步的對象衙传,`Objective-C的運行時都會給其分配一個遞歸鎖决帖,并且保存在一個哈希表中。
- 一個被加了同步的對象被析構(gòu)或者被置為nil都是沒有問題的蓖捶。然而文檔中并沒有對此進(jìn)行什么說明地回,所以我也不會在任何實際的代碼中依賴這個。
- 注意不要往
@synchronized
代碼塊中傳入nil
俊鱼!這會毀掉代碼的線程安全性刻像。通過往objc_sync_nil
加入斷點你可以看到這種情況的發(fā)生。
探究的下一步是研究synchronized代碼塊轉(zhuǎn)成匯編的代碼并闲,看看是否和我前面的例子相似细睡。我打賭synchronized
代碼塊轉(zhuǎn)換的匯編代碼不會和我們猜想的任何Objective-C代碼相似,上述的代碼例子只是@synchronized
實現(xiàn)的模型而已帝火。你能想到更好的模型嗎溜徙?或者在我的這些例子中哪里有瑕疵?請告訴我犀填。
-完-
[1] 遞歸鎖蠢壹,是一種在已持有鎖的線程重復(fù)請求鎖卻不會發(fā)生死鎖的鎖。你可以在這里找到一個相關(guān)的例子九巡。有個很好用的類NSRecursiveLock
图贸,它能實現(xiàn)這種效果,你可以試試。