當(dāng)你平時(shí)使用@ synchronized的時(shí)候有沒(méi)有想過(guò)下面幾個(gè)問(wèn)題:
1:鎖是如何與你傳入 @synchronized 的對(duì)象關(guān)聯(lián)上的?
2:@synchronized會(huì)保持(retain,增加引用計(jì)數(shù))被鎖住的對(duì)象么?
3:假如你傳入 @synchronized 的對(duì)象在 @synchronized 的 block 里面被釋放或者被賦值為 nil 將會(huì)怎么樣?
下面你帶著這幾個(gè)問(wèn)題來(lái)開(kāi)始今天的探索。
如果你已經(jīng)使用 Objective-C 編寫過(guò)任何并發(fā)程序,那么想必是見(jiàn)過(guò) @synchronized 這貨了。@synchronized 結(jié)構(gòu)所做的事情跟鎖(lock)類似:它防止不同的線程同時(shí)執(zhí)行同一段代碼。但在某些情況下,相比于使用 NSLock 創(chuàng)建鎖對(duì)象、加鎖和解鎖來(lái)說(shuō),@synchronized 用著更方便,可讀性更高。
如果你之前沒(méi)用過(guò) @synchronized,接下來(lái)有個(gè)使用它的例子。這篇文章實(shí)質(zhì)上是談?wù)動(dòng)嘘P(guān)我對(duì) @synchronized 實(shí)現(xiàn)原理的一個(gè)簡(jiǎn)短研究。
用到 @synchronized 的例子
假設(shè)我們正在用 Objective-C 實(shí)現(xiàn)一個(gè)線程安全的隊(duì)列,我們一開(kāi)始可能會(huì)這么干:
@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 類有個(gè) init 方法,它初始化了一個(gè) _elements 數(shù)組和一個(gè) NSLock 實(shí)例。這個(gè)類還有個(gè) push: 方法,它先獲取鎖、然后向數(shù)組中插入元素、最終釋放鎖??赡軙?huì)有許多線程同時(shí)調(diào)用 push: 方法,但是 [_elements addObject:element] 這行代碼在任何時(shí)候?qū)⒅粫?huì)在一個(gè)線程上運(yùn)行。步驟如下:
線程 A 調(diào)用 push: 方法
線程 B 調(diào)用 push: 方法
線程 B 調(diào)用 [_lock lock] - 因?yàn)楫?dāng)前沒(méi)有其他線程持有鎖,線程 B 獲得了鎖
線程 A 調(diào)用 [_lock lock],但是鎖已經(jīng)被線程 B 占了所以方法調(diào)用并沒(méi)有返回-這會(huì)暫停線程 A 的執(zhí)行
線程 B 向 _elements 添加元素后調(diào)用 [_lock unlock]。當(dāng)這些發(fā)生時(shí),線程 A 的 [_lock lock] 方法返回,并繼續(xù)將自己的元素插入 _elements。
我們可以用 @synchronized 結(jié)構(gòu)更簡(jiǎn)要地實(shí)現(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 block” 與 [_lock lock] 和 [_lock unlock] 效果相同。你可以把它當(dāng)成是鎖住 self,仿佛 self 就是個(gè) NSLock。鎖在左括號(hào) { 后面的任何代碼運(yùn)行之前被獲取到,在右括號(hào) } 后面的任何代碼運(yùn)行之前被釋放掉。這爽就爽在媽媽再也不用擔(dān)心我忘記調(diào)用 unlock 了!
你可以給任何 Objective-C 對(duì)象上加個(gè) @synchronized。那么我們也可以在上面的例子中用 @synchronized(_elements) 來(lái)替代 @synchronized(self),效果是相同的。
回到研究上來(lái)
我對(duì) @synchronized 的實(shí)現(xiàn)十分好奇并搜了一些它的細(xì)節(jié)。我找到了一些答案,但這些解釋都沒(méi)有達(dá)到我想要的深度。鎖是如何與你傳入 @synchronized 的對(duì)象關(guān)聯(lián)上的?@synchronized會(huì)保持(retain,增加引用計(jì)數(shù))被鎖住的對(duì)象么?假如你傳入 @synchronized 的對(duì)象在 @synchronized 的 block 里面被釋放或者被賦值為 nil 將會(huì)怎么樣?這些全都是我想回答的問(wèn)題。而我這次的收獲,會(huì)要你好看。
@synchronized 的文檔告訴我們 @synchronized block 在被保護(hù)的代碼上暗中添加了一個(gè)異常處理。為的是同步某對(duì)象時(shí)如若拋出異常,鎖會(huì)被釋放掉。
SO 上的這篇帖子 說(shuō) @synchronized block 會(huì)變成 objc_sync_enter 和 objc_sync_exit 的成對(duì)兒調(diào)用。我們不知道這些函數(shù)是干啥的,但基于這些事實(shí)我們可以認(rèn)為編譯器將這樣的代碼:
@synchronized(obj) {
// do work
}
轉(zhuǎn)化成這樣的東東:
@try {
objc_sync_enter(obj);
// do work
} @finally {
objc_sync_exit(obj);
}
objc_sync_enter 和 objc_sync_exit 是什么鬼?它們是如何實(shí)現(xiàn)的?在 Xcode 中按住 Command 鍵單擊它們,然后進(jìn)到了,里面有我們感興趣的這兩個(gè)函數(shù):
/**
* Begin synchronizing on 'obj'.
* Allocates recursive pthread_mutex associated with 'obj' if needed.
*
* @param obj The object to begin synchronizing on.
*
* @return OBJC_SYNC_SUCCESS once lock is acquired.
*/
OBJC_EXPORT int objc_sync_enter(id obj)
__OSX_AVAILABLE_STARTING(__MAC_10_3, __IPHONE_2_0);
/**
* End synchronizing on 'obj'.
*
* @param obj The objet to end synchronizing on.
*
* @return OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
*/
OBJC_EXPORT int objc_sync_exit(id obj)
__OSX_AVAILABLE_STARTING(__MAC_10_3, __IPHONE_2_0);
文件底部的一句話提醒著我們:蘋果工程師也是人啊哈哈
// The wait/notify functions have never worked correctly and no longer exist.
OBJC_EXPORT int objc_sync_wait(id obj, long long milliSecondsMaxWait)
UNAVAILABLE_ATTRIBUTE;
OBJC_EXPORT int objc_sync_notify(id obj)
UNAVAILABLE_ATTRIBUTE;
OBJC_EXPORT int objc_sync_notifyAll(id obj)
UNAVAILABLE_ATTRIBUTE;
譯者注: 此處原文摘抄的源碼較舊,所以我替換上了最新的頭文件源碼。
不過(guò),objc_sync_enter 的文檔告訴我們一些新東西: @synchronized 結(jié)構(gòu)在工作時(shí)為傳入的對(duì)象分配了一個(gè)遞歸鎖。分配工作何時(shí)發(fā)生,如何發(fā)生呢?它怎樣處理 nil?幸運(yùn)的是 Objective-C runtime 是開(kāi)源的,所以我們可以馬上閱讀源碼并找到答案!
注:遞歸鎖在被同一線程重復(fù)獲取時(shí)不會(huì)產(chǎn)生死鎖。你可以在這找到一個(gè)它工作原理的精巧案例。有個(gè)叫做 NSRecursiveLock 的現(xiàn)成的類也是這樣的,你可以試試。
你可以在這里找到 objc-sync 的全部源碼,但我要帶著你看源碼,讓你屌的飛起。我們先從文件頂部的數(shù)據(jù)結(jié)構(gòu)開(kāi)始看。在代碼塊的下方我將立刻做出解釋,所以嘗試?yán)斫獯a時(shí)別花太長(zhǎng)時(shí)間哦。
typedef struct SyncData {
id object;
recursive_mutex_t mutex;
struct SyncData* nextData;
int threadCount;
} SyncData;
typedef struct SyncList {
SyncData *data;
spinlock_t lock;
} SyncList;
// Use multiple parallel lists to decrease contention among unrelated objects.
#define COUNT 16
#define HASH(obj) ((((uintptr_t)(obj)) >> 5) & (COUNT - 1))
#define LOCK_FOR_OBJ(obj) sDataLists[HASH(obj)].lock
#define LIST_FOR_OBJ(obj) sDataLists[HASH(obj)].data
static SyncList sDataLists[COUNT];
一開(kāi)始,我們有一個(gè) struct SyncData 的定義。這個(gè)結(jié)構(gòu)體包含一個(gè) object(嗯就是我們給 @synchronized 傳入的那個(gè)對(duì)象)和一個(gè)有關(guān)聯(lián)的 recursive_mutex_t,它就是那個(gè)跟 object 關(guān)聯(lián)在一起的鎖。每個(gè) SyncData 也包含一個(gè)指向另一個(gè) SyncData 對(duì)象的指針,叫做 nextData,所以你可以把每個(gè) SyncData 結(jié)構(gòu)體看做是鏈表中的一個(gè)元素。最后,每個(gè) SyncData 包含一個(gè) threadCount,這個(gè) SyncData 對(duì)象中的鎖會(huì)被一些線程使用或等待,threadCount 就是此時(shí)這些線程的數(shù)量。它很有用處,因?yàn)?SyncData 結(jié)構(gòu)體會(huì)被緩存,threadCount==0 就暗示了這個(gè) SyncData 實(shí)例可以被復(fù)用。
下面是 struct SyncList 的定義。正如我在上面提過(guò),你可以把 SyncData 當(dāng)做是鏈表中的節(jié)點(diǎn)。每個(gè) SyncList 結(jié)構(gòu)體都有個(gè)指向 SyncData 節(jié)點(diǎn)鏈表頭部的指針,也有一個(gè)用于防止多個(gè)線程對(duì)此列表做并發(fā)修改的鎖。
上面代碼塊的最后一行是 sDataLists 的聲明 - 一個(gè) SyncList 結(jié)構(gòu)體數(shù)組,大小為16。通過(guò)定義的一個(gè)哈希算法將傳入對(duì)象映射到數(shù)組上的一個(gè)下標(biāo)。值得注意的是這個(gè)哈希算法設(shè)計(jì)的很巧妙,是將對(duì)象指針在內(nèi)存的地址轉(zhuǎn)化為無(wú)符號(hào)整型并右移五位,再跟 0xF 做按位與運(yùn)算,這樣結(jié)果不會(huì)超出數(shù)組大小。 LOCK_FOR_OBJ(obj) 和 LIST_FOR_OBJ(obj) 這倆宏就更好理解了,先是哈希出對(duì)象的數(shù)組下標(biāo),然后取出數(shù)組對(duì)應(yīng)元素的 lock 或 data。一切都是這么順理成章哈。
當(dāng)你調(diào)用 objc_sync_enter(obj) 時(shí),它用 obj 內(nèi)存地址的哈希值查找合適的 SyncData,然后將其上鎖。當(dāng)你調(diào)用 objc_sync_exit(obj) 時(shí),它查找合適的 SyncData 并將其解鎖。
譯者注:上面的源碼和幾段解釋有些原文解釋不清和疏漏的地方,我看了源碼后按照自己的理解進(jìn)行了補(bǔ)充和修正。
噢耶!現(xiàn)在我們知道了 @synchronized 如何將一個(gè)鎖和你正在同步的對(duì)象關(guān)聯(lián)起來(lái),我希望聊聊當(dāng)一個(gè)對(duì)象在 @synchronized block 當(dāng)中被釋放或設(shè)為 nil 時(shí)會(huì)發(fā)生什么。
如果你看了源碼,你會(huì)注意到 objc_sync_enter 里面沒(méi)有 retain 和 release。所以它要么沒(méi)有保持傳遞給它的對(duì)象,要么或是在 ARC 下被編譯。我們可以用下面的代碼來(lái)做個(gè)測(cè)試:
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]));
}
兩次輸出結(jié)果都是 1。那么 objc_sync_enter 貌似是沒(méi)保持被傳入的對(duì)象啊。這就有趣了。如果你正在同步的對(duì)象被釋放了,然后有可能另一個(gè)新的對(duì)象在此處(被釋放對(duì)象的內(nèi)存地址)被分配內(nèi)存。有可能某個(gè)其他的線程試著去同步那個(gè)新的對(duì)象(就是那個(gè)在被釋放的舊對(duì)象的內(nèi)存地址上剛剛新創(chuàng)建的對(duì)象)。在這種情況下,另一個(gè)線程將會(huì)阻塞,直到當(dāng)前線程結(jié)束它的同步 block。這看起來(lái)并不是很糟。這聽(tīng)起來(lái)像是這種事情實(shí)現(xiàn)者早就知道并予以接受。我沒(méi)有遇到過(guò)任何好的替代方案。
假如對(duì)象在 “synchronized block” 中被設(shè)成 nil 呢?我們回顧下我們“拿衣服(naive)”的實(shí)現(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);
}
objc_sync_enter 被調(diào)用時(shí)傳入的是 test 而 objc_sync_exit 被調(diào)用時(shí)傳入的是 nil。而傳入 nil 的時(shí)候 objc_sync_exit 是個(gè)空操作,所以將不會(huì)有人釋放鎖。這真操蛋!
如果 Objective-C 容易受這種情況的影響,我們知道么?下面的代碼調(diào)用 @synchronized 并在 @synchronized block 中將一個(gè)指針設(shè)為 nil。然后在后臺(tái)線程對(duì)指向同一個(gè)對(duì)象的指針調(diào)用 @synchronized。如果在 @synchronized block 中設(shè)置一個(gè)對(duì)象為 nil 會(huì)讓鎖死鎖,那么在第二個(gè) @synchronized 中的代碼將永遠(yuǎn)不會(huì)執(zhí)行。我們將不會(huì)在控制臺(tái)中看見(jiàn)任何東西打印出來(lái)。
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)我們執(zhí)行上面的代碼時(shí),那行代碼確實(shí)打印到控制臺(tái)了!所以 Objective-C 很好地處理了這種情形。我打賭是編譯器做了類似下面的事情來(lái)解決這事兒的。
NSString *test = @"test";
id synchronizeTarget = (id)test;
@try {
objc_sync_enter(synchronizeTarget);
test = nil;
} @finally {
objc_sync_exit(synchronizeTarget);
}
用這種方式實(shí)現(xiàn)的話,傳遞給 objc_sync_enter 和 objc_sync_exit 總是相同的對(duì)象。他們?cè)趥魅?nil 時(shí)都是空操作。這帶來(lái)了個(gè)棘手的 debug 場(chǎng)景:如果你向 @synchronized 傳遞 nil,那么你就不會(huì)得到任何鎖而且你的代碼將不會(huì)是線程安全的!如果你想知道為什么你正收到出乎意料的競(jìng)態(tài)(race),確保你沒(méi)向你的 @synchronized 傳入 nil。你可以在 objc_sync_nil 上設(shè)置一個(gè)符號(hào)斷點(diǎn)來(lái)達(dá)到此目的。objc_sync_nil 是一個(gè)空方法,當(dāng) objc_sync_enter 函數(shù)被傳入 nil 時(shí)會(huì)被調(diào)用,折讓 debug 更容易些。
譯者注:下面是 objc_sync_enter 的源碼,主要邏輯很容易看懂,加深理解 objc_sync_nil:
int objc_sync_enter(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
SyncData* data = id2data(obj, ACQUIRE);
require_action_string(data != NULL, done, result = OBJC_SYNC_NOT_INITIALIZED, "id2data failed");
result = recursive_mutex_lock(&data->mutex);
require_noerr_string(result, done, "mutex_lock failed");
} else {
// @synchronized(nil) does nothing
if (DebugNilSync) {
_objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
}
objc_sync_nil();
}
done:
return result;
}
這回答了我眼下的問(wèn)題。
你調(diào)用 sychronized 的每個(gè)對(duì)象,Objective-C runtime 都會(huì)為其分配一個(gè)遞歸鎖并存儲(chǔ)在哈希表中。
如果在 sychronized 內(nèi)部對(duì)象被釋放或被設(shè)為 nil 看起來(lái)都 OK。不過(guò)這沒(méi)在文檔中說(shuō)明,所以我不會(huì)再生產(chǎn)代碼中依賴這條。
注意不要向你的 sychronized block 傳入 nil!這將會(huì)從代碼中移走線程安全。你可以通過(guò)在 objc_sync_nil 上加斷點(diǎn)來(lái)查看是否發(fā)生了這樣的事情。
研究的下一步將是研究下 “synchronized block” 輸出的匯編,看看它是否跟我上面的例子相似。我打賭 @synchronized block 的匯編輸出不會(huì)跟任何我們?cè)O(shè)計(jì)的 Objective-C 代碼相同,上面的代碼充其量是 @synchronized 的工作模型。你能想到更好的模型么?我的模型在哪些情形下會(huì)有瑕疵么?告訴我吧!