關於@synchronized 比你想知道的還多

若是你曾經使用Objective-C作過併發編程,那你確定見過@synchronized這個結構。@synchronized這個結構發揮了和鎖同樣的做用:它避免了多個線程同時執行同一段代碼。和使用NSLock進行建立鎖、加鎖、解鎖相比,在某些狀況下@synchronized會更方便、更易讀。html

若是你歷來沒有使用過@synchronized,具體如何使用能夠參考下面的實例。本文的將圍繞我對@synchronized的原理的探究進行講述。objective-c

使用@synchronized的例子

假如要用Objective-C實現一個線程安全的隊列,咱們大概會這樣寫:編程

@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數組和一個NSLock。另外,有一個須要獲取這個鎖以插入元素到數組中而後釋放鎖的push:方法。許多線程會同時調用push:方法,然而[ _elements addObject:element];這行代碼也只能同時被一條線程訪問。這個流程應該是這樣的:數組

  1. 線程A調用push:方法
  2. 線程B調用push:方法
  3. 線程B調用[_lock lock],由於沒有其餘線程持有這個鎖,所以線程B取得了這個鎖
  4. 線程A調用[_lock lock],可是此時這個鎖被線程B所持有,因此這個方法調用並無返回,使線程A暫停了執行
  5. 線程B添加了一個元素到_elements中,而後調用[ _lock unlock]方法。此時,線程A的[ _lock unlock]方法返回了,接着繼續執行線程A的元素插入操做

使用@synchronized,咱們能夠更簡潔明瞭的實現剛纔的功能:緩存

@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看成一個NSLock來對self進行加鎖。在運行{後的代碼前獲取鎖,並在運行}後的其餘代碼前釋放這個鎖。這很是的方便,由於這意味着你永遠不會忘了調用unlock安全

你也能夠在任何Objective-C的對象上使用@synchronized。所以,一樣的咱們也能夠像下面的例子裏同樣,使用@synchronized(_elements)來代替@synchronized(self),這二者的效果是一致的。ruby

回到個人探究上來

我對@synchronized的實現很好奇,因而我在谷歌搜索了它的一些細節。我找到了關於這個的一些回答 @synchronized是如何加鎖/解鎖的 在@synchronized中改變加鎖的對象 Apple的文檔,但沒有一個答案能給我足夠深刻的解釋。傳入@synchronized的參數和這個鎖有什麼關係?@synchronized是否持有它所加鎖的對象?若是傳入@synchronized代碼塊的對象在代碼塊裏被析構了或者被置爲nil了會怎麼樣?這些都是我想問的問題。在下文中,我會分享個人發現。數據結構

關於@synchronized的Apple的文檔中提到,@synchronized代碼塊隱式地給被保護的代碼段添加了一個異常處理塊。這就是爲何在給某個對象保持同步的時候,若是拋出了異常,鎖就會被釋放。多線程

stackoverflow的一個回答中提到,@synchronized塊會轉化成一對objc_sync_enterobjc_sync_exit的函數調用。咱們並不知道這些函數都幹了什麼,可是根據這個咱們能夠推斷,編譯器會像這樣轉化代碼:併發

@synchronized(obj) {
    // do work } 

轉換成大概像這樣的:

@try {
    objc_sync_enter(obj);
    // do work } @finally { objc_sync_exit(obj); } 

具體什麼是objc_sync_enterobject_sync_exit以及它們是如何實現的,咱們經過Command+點擊這兩個函數跳轉到了<objc/objc-sync.h>裏,這裏有咱們要找的兩個函數:

// 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); 

總之,關於objc_sync_enter的文檔告訴了咱們:@synchronized是基於一個遞歸鎖[1] 來傳遞一個對象的。何時分配內存、如何分配內存的?如何處理nil值?幸運的是,Objective-C運行時是開源的,因此咱們能夠閱讀它的源碼找到答案。

你能夠在這裏查看全部objc-sync的源碼,可是我會領你在更高的層面通讀這些源碼。咱們先從文件頂部的數據結構看起。我會爲你解釋下面的源碼所以你沒必要花時間來嘗試解讀這些代碼。

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]; 

首先,咱們看到告終構體struct SyncData的定義。這個結構體包含了一個object(傳入@synchronized的對象)還有一個關聯着這個鎖以及被鎖對象的recursive_mutex_t。每一個SyncData含有一個指向其餘SyncData的指針nextData,所以你能夠認爲每一個SyncData都是鏈表裏的一個節點。最後,每一個SyncData含有一個threadCount來表示在使用或者等待鎖的線程的數量。這頗有用,由於SyncData是被緩存的,當threadCount == 0時,表示一個SyncData的實例能被複用。

接着,咱們有了struct SyncList的定義。正如我在前文中所提到的,你能夠把一個SyncData看成鏈表中的一個節點。每一個SyncList結構都有一個指向SyncData鏈表頭部的指針,就像一個用於避免多線程併發的修改該鏈表的鎖同樣。

這個代碼塊的最後一行之上是一個sDataLists的定義,這是一個SyncList結構的數組。剛開始可能看起來不太像,但這個sDataList數組是一個哈希表(相似NSDictionary),用於把Objectice-C對象映射到他們對應的鎖。

當你調用objc_sync_enter(obj)的時候,它經過一個記錄obj地址的哈希表來找到對應的SyncData,而後對其加鎖。當你調用objc_sync_exit的時候,它以一樣的方式找到對應的SyncData並將其解鎖。

很好!如今咱們知道了@synchronized是如何關聯一個鎖和那個被加同步鎖的對象,接下來,我會講講當一個對象在@synchronized代碼塊中被析構或者被置nil會發生什麼。

若是你看源碼的話,你會發現objc_sync_enter裏面並無retains或者release。所以,它並不會持有傳入的對象,或者也有多是由於它是在arc中編譯的。咱們能夠經過如下的代碼來進行測試:

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])); } 

對於每一個的持有數,輸出總爲1。所以objc_sync_enter不會持有傳入的對象。這頗有意思。若是你須要同步的對象唄析構了,而後可能另一個新的對象被分配到了這個內存地址上,極可能其餘線程正嘗試同步那個有着和原對象有着相同地址的新的對象。在這種狀況下,其餘線程會被阻塞直到當前線程完成了本身的同步代碼塊。這彷佛沒什麼毛病。這聽起來像這種實現是已被知曉的並且也沒什麼問題。我並無看到其餘更好的替代方案。

那若是這個對象在@synchronized代碼塊中被設成nil會怎樣呢?再來看看咱們的實現:

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的時候傳入test,調用objc_sync_exit的時候傳入nil。若objc_sync_exit傳入nil的時候什麼都不作,那麼也再也不會有人去釋放這個鎖。這很糟糕。

Objective-C會那麼輕易的被這種問題影響嗎?下面的代碼把一個會被置nil的指針傳入@synchronized。而後在後臺線程中往@synchronized中傳入一個指向同一對象的指針。若是在@synchronized中把一個對象置爲nil讓這個鎖處於加鎖的狀態,那麼在第二個@synchronized中的代碼將永遠不會被運行。在控制檯中咱們應該什麼都看不到。

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"); } }); 

當咱們運行上述代碼時,這行代碼卻的確被打印到控制檯上了!所以能夠證實,Objective-C能很好的處理這種狀況。我打賭這種狀況是被編譯器處理過的,大概以下:

NSString *test = @"test"; id synchronizeTarget = (id)test; @try { objc_sync_enter(synchronizeTarget); test = nil; } @finally { objc_sync_exit(synchronizeTarget); } 

有了這種實現,傳入objc_sync_enterobjc_sync_exit的對象老是相同的。當傳入nil的時候他們什麼都不會作。這引出了一個很棘手的debug場景:若是你往@synchronized裏傳入nil,那麼至關於你並無進行過加鎖操做,同時你的代碼將再也不是線程安全的了!若是你被莫名其妙的問題困擾着,那麼先確保你沒有把nil傳入你的@synchronized代碼塊。你能夠經過給objc_sync_nil設置一個符號斷點來檢查,objc_sync_nil是一個空方法,會在往objc_sync_enter傳入nil的時候調用,這會讓調試方便的多。

如今,個人問題獲得了回答。

  1. 對於每一個加了同步的對象,`Objective-C的運行時都會給其分配一個遞歸鎖,而且保存在一個哈希表中。
  2. 一個被加了同步的對象被析構或者被置爲nil都是沒有問題的。然而文檔中並無對此進行什麼說明,因此我也不會在任何實際的代碼中依賴這個。
  3. 注意不要往@synchronized代碼塊中傳入nil!這會毀掉代碼的線程安全性。經過往objc_sync_nil加入斷點你能夠看到這種狀況的發生。

探究的下一步是研究synchronized代碼塊轉成彙編的代碼,看看是否和我前面的例子類似。我打賭synchronized代碼塊轉換的彙編代碼不會和咱們猜測的任何Objective-C代碼類似,上述的代碼例子只是@synchronized實現的模型而已。你能想到更好的模型嗎?或者在個人這些例子中哪裏有瑕疵?請告訴我。

-完-

[1] 遞歸鎖,是一種在已持有鎖的線程重複請求鎖卻不會發生死鎖的鎖。你能夠在這裏找到一個相關的例子。有個很好用的類NSRecursiveLock,它能實現這種效果,你能夠試試。

相關文章
相關標籤/搜索