iOS開發筆記--關於 @synchronized,這兒比你想知道的還要多

如果你已經使用 Objective-C 編寫過任何併發程序,那麼想必是見過 @synchronized 這貨了。@synchronized 結構所做的事情跟鎖(lock)類似:它防止不同的線程同時執行同一段代碼。但在某些情況下,相比於使用 NSLock 創建鎖對象、加鎖和解鎖來說,@synchronized 用着更方便,可讀性更高。

譯者注:這與蘋果官方文檔對 @synchronized 的介紹有少許出入,但意思差不多。蘋果官方文檔更強調它“防止不同的線程同時獲取相同的鎖”,因爲文檔在集中介紹多線程編程各種鎖的作用,所以更強調“相同的鎖”而不是“同一段代碼”。

如果你之前沒用過 @synchronized,接下來有個使用它的例子。這篇文章實質上是談談有關我對 @synchronized 實現原理的一個簡短研究。

用到 @synchronized 的例子

假設我們正在用 Objective-C 實現一個線程安全的隊列,我們一開始可能會這麼幹:

<code class="hljs objectivec has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-class" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">@implementation</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">ThreadSafeQueue</span></span>
{
    <span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">NSMutableArray</span> *_elements;
    NSLock *_lock;
}
- (instancetype)init
{
    <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">self</span> = [<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">super</span> init];
    <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">if</span> (<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">self</span>) {
        _elements = [<span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">NSMutableArray</span> array];
        _lock = [[NSLock alloc] init];
    }
    <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">self</span>;
}
- (<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span>)push:(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">id</span>)element
{
    [_lock lock];
    [_elements addObject:element];
    [_lock unlock];
}
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">@end</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li><li style="box-sizing: border-box; padding: 0px 5px;">20</li><li style="box-sizing: border-box; padding: 0px 5px;">21</li></ul>

上面的 ThreadSafeQueue 類有個 init 方法,它初始化了一個 _elements 數組和一個 NSLock 實例。這個類還有個 push: 方法,它先獲取鎖、然後向數組中插入元素、最終釋放鎖。可能會有許多線程同時調用 push: 方法,但是 [_elements addObject:element] 這行代碼在任何時候將只會在一個線程上運行。步驟如下:

線程 A 調用 push: 方法

線程 B 調用 push: 方法

線程 B 調用 [_lock lock] - 因爲當前沒有其他線程持有鎖,線程 B 獲得了鎖

線程 A 調用 [_lock lock],但是鎖已經被線程 B 佔了所以方法調用並沒有返回-這會暫停線程 A 的執行

線程 B 向 _elements 添加元素後調用 [_lock unlock]。當這些發生時,線程 A 的 [_lock lock] 方法返回,並繼續將自己的元素插入 _elements。

我們可以用 @synchronized 結構更簡要地實現這些:

<code class="hljs objectivec has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-class" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">@implementation</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">ThreadSafeQueue</span></span>
{
    <span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">NSMutableArray</span> *_elements;
}
- (instancetype)init
{
    <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">self</span> = [<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">super</span> init];
    <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">if</span> (<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">self</span>) {
        _elements = [<span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">NSMutableArray</span> array];
    }
    <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">self</span>;
}
- (<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span>)increment
{
    @synchronized (<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">self</span>) {
        [_elements addObject:element];
    }
}
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">@end</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li></ul>

在前面的例子中,”synchronized block” 與 [_lock lock] 和 [_lock unlock] 效果相同。你可以把它當成是鎖住 self,彷彿 self 就是個 NSLock。鎖在左括號 { 後面的任何代碼運行之前被獲取到,在右括號 } 後面的任何代碼運行之前被釋放掉。這爽就爽在媽媽再也不用擔心我忘記調用 unlock 了!

你可以給任何 Objective-C 對象上加個 @synchronized。那麼我們也可以在上面的例子中用 @synchronized(_elements) 來替代 @synchronized(self),效果是相同的。

回到研究上來

我對 @synchronized 的實現十分好奇並搜了一些它的細節。我找到了一些答案,但這些解釋都沒有達到我想要的深度。鎖是如何與你傳入 @synchronized 的對象關聯上的?@synchronized會保持(retain,增加引用計數)被鎖住的對象麼?假如你傳入 @synchronized 的對象在 @synchronized 的 block 裏面被釋放或者被賦值爲 nil 將會怎麼樣?這些全都是我想回答的問題。而我這次的收穫,會要你好看。

@synchronized 的文檔告訴我們 @synchronized block 在被保護的代碼上暗中添加了一個異常處理。爲的是同步某對象時如若拋出異常,鎖會被釋放掉。

SO 上的這篇帖子 說 @synchronized block 會變成 objc_sync_enter 和 objc_sync_exit 的成對兒調用。我們不知道這些函數是幹啥的,但基於這些事實我們可以認爲編譯器將這樣的代碼:

<code class="hljs ruby has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-variable" style="color: rgb(102, 0, 102); box-sizing: border-box;">@synchronized</span>(obj) {
    <span class="hljs-regexp" style="color: rgb(0, 136, 0); box-sizing: border-box;">//</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">do</span> work
}</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li></ul>

轉化成這樣的東東

<code class="hljs scss has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-at_rule" style="box-sizing: border-box;">@try {</span>
    <span class="hljs-function" style="box-sizing: border-box;">objc_sync_enter(obj)</span>;
    <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// do work</span>
} <span class="hljs-at_rule" style="box-sizing: border-box;">@finally {</span>
    <span class="hljs-function" style="box-sizing: border-box;">objc_sync_exit(obj)</span>;    
}</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li></ul>

objc_sync_enter 和 objc_sync_exit 是什麼鬼?它們是如何實現的?在 Xcode 中按住 Command 鍵單擊它們,然後進到了,裏面有我們感興趣的這兩個函數:

<code class="hljs java has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-javadoc" style="color: rgb(136, 0, 0); box-sizing: border-box;">/** 
 * Begin synchronizing on 'obj'.  
 * Allocates recursive pthread_mutex associated with 'obj' if needed.
 * 
 *<span class="hljs-javadoctag" style="color: rgb(102, 0, 102); box-sizing: border-box;"> @param</span> obj The object to begin synchronizing on.
 * 
 *<span class="hljs-javadoctag" style="color: rgb(102, 0, 102); box-sizing: border-box;"> @return</span> OBJC_SYNC_SUCCESS once lock is acquired.  
 */</span>
OBJC_EXPORT  <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">int</span> objc_sync_enter(id obj)
    __OSX_AVAILABLE_STARTING(__MAC_10_3, __IPHONE_2_0);
<span class="hljs-javadoc" style="color: rgb(136, 0, 0); box-sizing: border-box;">/** 
 * End synchronizing on 'obj'. 
 * 
 *<span class="hljs-javadoctag" style="color: rgb(102, 0, 102); box-sizing: border-box;"> @param</span> obj The objet to end synchronizing on.
 * 
 *<span class="hljs-javadoctag" style="color: rgb(102, 0, 102); box-sizing: border-box;"> @return</span> OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
 */</span>
OBJC_EXPORT  <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">int</span> objc_sync_exit(id obj)
    __OSX_AVAILABLE_STARTING(__MAC_10_3, __IPHONE_2_0);</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li></ul>

文件底部的一句話提醒着我們:蘋果工程師也是人啊哈哈

<code class="hljs objectivec has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// The wait/notify functions have never worked correctly and no longer exist.</span>
OBJC_EXPORT  <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">int</span> objc_sync_wait(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">id</span> obj, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">long</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">long</span> milliSecondsMaxWait) 
    UNAVAILABLE_ATTRIBUTE;
OBJC_EXPORT  <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">int</span> objc_sync_notify(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">id</span> obj) 
    UNAVAILABLE_ATTRIBUTE;
OBJC_EXPORT  <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">int</span> objc_sync_notifyAll(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">id</span> obj) 
    UNAVAILABLE_ATTRIBUTE</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li></ul>

譯者注: 此處原文摘抄的源碼較舊,所以我替換上了最新的頭文件源碼。

不過,objc_sync_enter 的文檔告訴我們一些新東西: @synchronized 結構在工作時爲傳入的對象分配了一個遞歸鎖。分配工作何時發生,如何發生呢?它怎樣處理 nil?幸運的是 Objective-C runtime 是開源的,所以我們可以馬上閱讀源碼並找到答案!

注:遞歸鎖在被同一線程重複獲取時不會產生死鎖。你可以在這找到一個它工作原理的精巧案例。有個叫做 NSRecursiveLock 的現成的類也是這樣的,你可以試試。

你可以在這裏找到 objc-sync 的全部源碼,但我要帶着你看源碼,讓你屌的飛起。我們先從文件頂部的數據結構開始看。在代碼塊的下方我將立刻做出解釋,所以嘗試理解代碼時別花太長時間哦。

<code class="hljs cs has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">typedef <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">struct</span> SyncData {
    id <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">object</span>;
    recursive_mutex_t mutex;
    <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">struct</span> SyncData* nextData;
    <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">int</span> threadCount;
} SyncData;
typedef <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">struct</span> SyncList {
    SyncData *data;
    spinlock_t <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">lock</span>;
} SyncList;
<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// Use multiple parallel lists to decrease contention among unrelated objects.</span>
<span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> COUNT 16</span>
<span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> HASH(obj) ((((uintptr_t)(obj)) >> 5) & (COUNT - 1))</span>
<span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> LOCK_FOR_OBJ(obj) sDataLists[HASH(obj)].lock</span>
<span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> LIST_FOR_OBJ(obj) sDataLists[HASH(obj)].data</span>
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">static</span> SyncList sDataLists[COUNT];</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li></ul>

一開始,我們有一個 struct SyncData 的定義。這個結構體包含一個 object(嗯就是我們給 @synchronized 傳入的那個對象)和一個有關聯的 recursive_mutex_t,它就是那個跟 object 關聯在一起的鎖。每個 SyncData 也包含一個指向另一個 SyncData 對象的指針,叫做 nextData,所以你可以把每個 SyncData 結構體看做是鏈表中的一個元素。最後,每個 SyncData 包含一個 threadCount,這個 SyncData 對象中的鎖會被一些線程使用或等待,threadCount 就是此時這些線程的數量。它很有用處,因爲 SyncData 結構體會被緩存,threadCount==0 就暗示了這個 SyncData 實例可以被複用。

下面是 struct SyncList 的定義。正如我在上面提過,你可以把 SyncData 當做是鏈表中的節點。每個 SyncList 結構體都有個指向 SyncData 節點鏈表頭部的指針,也有一個用於防止多個線程對此列表做併發修改的鎖。

上面代碼塊的最後一行是 sDataLists 的聲明 - 一個 SyncList 結構體數組,大小爲16。通過定義的一個哈希算法將傳入對象映射到數組上的一個下標。值得注意的是這個哈希算法設計的很巧妙,是將對象指針在內存的地址轉化爲無符號整型並右移五位,再跟 0xF 做按位與運算,這樣結果不會超出數組大小。 LOCK_FOR_OBJ(obj) 和 LIST_FOR_OBJ(obj) 這倆宏就更好理解了,先是哈希出對象的數組下標,然後取出數組對應元素的 lock 或 data。一切都是這麼順理成章哈。

當你調用 objc_sync_enter(obj) 時,它用 obj 內存地址的哈希值查找合適的 SyncData,然後將其上鎖。當你調用 objc_sync_exit(obj) 時,它查找合適的 SyncData 並將其解鎖。

譯者注:上面的源碼和幾段解釋有些原文解釋不清和疏漏的地方,我看了源碼後按照自己的理解進行了補充和修正。

噢耶!現在我們知道了 @synchronized 如何將一個鎖和你正在同步的對象關聯起來,我希望聊聊當一個對象在 @synchronized block 當中被釋放或設爲 nil 時會發生什麼。

如果你看了源碼,你會注意到 objc_sync_enter 裏面沒有 retain 和 release。所以它要麼沒有保持傳遞給它的對象,要麼或是在 ARC 下被編譯。我們可以用下面的代碼來做個測試:

<code class="hljs autohotkey has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">NSDate *test = [NSDate date]<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">;</span>
// This should always be <span class="hljs-escape" style="box-sizing: border-box;">`1</span><span class="hljs-escape" style="box-sizing: border-box;">`
</span>NSLog(@<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"%@"</span>, @([test retainCount]))<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">;</span>
@synchronized (test) {
    // This will be <span class="hljs-escape" style="box-sizing: border-box;">`2</span><span class="hljs-escape" style="box-sizing: border-box;">` </span><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">if</span> <span class="hljs-escape" style="box-sizing: border-box;">`@</span>synchronized<span class="hljs-escape" style="box-sizing: border-box;">` </span>somehow
    // retains <span class="hljs-escape" style="box-sizing: border-box;">`t</span>est<span class="hljs-escape" style="box-sizing: border-box;">`
</span>    NSLog(@<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"%@"</span>, @([test retainCount]))<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">;</span>
}</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li></ul>

兩次輸出結果都是 1。那麼 objc_sync_enter 貌似是沒保持被傳入的對象啊。這就有趣了。如果你正在同步的對象被釋放了,然後有可能另一個新的對象在此處(被釋放對象的內存地址)被分配內存。有可能某個其他的線程試着去同步那個新的對象(就是那個在被釋放的舊對象的內存地址上剛剛新創建的對象)。在這種情況下,另一個線程將會阻塞,直到當前線程結束它的同步 block。這看起來並不是很糟。這聽起來像是這種事情實現者早就知道並予以接受。我沒有遇到過任何好的替代方案。

假如對象在 “synchronized block” 中被設成 nil 呢?我們回顧下我們“拿衣服(naive)”的實現吧:

<code class="hljs sql has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">NSString *test = @"test";
@try {
    // Allocates a <span class="hljs-operator" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">lock</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">for</span> test <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">and</span> locks it
    objc_sync_enter(test);</span>
    test = nil;
} @finally {
    // Passed `nil`, so the <span class="hljs-operator" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">lock</span> allocated <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">in</span> <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">`objc_sync_enter`</span>
    // above <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">is</span> never unlocked <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">or</span> deallocated
    objc_sync_exit(test);</span>   
}</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li></ul>

objc_sync_enter 被調用時傳入的是 test 而 objc_sync_exit 被調用時傳入的是 nil。而傳入 nil 的時候 objc_sync_exit 是個空操作,所以將不會有人釋放鎖。這真操蛋!

如果 Objective-C 容易受這種情況的影響,我們知道麼?下面的代碼調用 @synchronized 並在 @synchronized block 中將一個指針設爲 nil。然後在後臺線程對指向同一個對象的指針調用 @synchronized。如果在 @synchronized block 中設置一個對象爲 nil 會讓鎖死鎖,那麼在第二個 @synchronized 中的代碼將永遠不會執行。我們將不會在控制檯中看見任何東西打印出來。

<code class="hljs applescript has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">NSNumber *<span class="hljs-type" style="box-sizing: border-box;">number</span> = @(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1</span>);
NSNumber *thisPtrWillGoToNil = <span class="hljs-type" style="box-sizing: border-box;">number</span>;
@synchronized (thisPtrWillGoToNil) {
    /**
     * Here we <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">set</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">the</span> thing <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">that</span> we're synchronizing <span class="hljs-function_start" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">on</span></span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">to</span> `nil`. If
     * implemented naively, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">the</span> object would be passed <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">to</span> `objc_sync_enter`
     * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">and</span> `nil` would be passed <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">to</span> `objc_sync_exit`, causing a lock <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">to</span>
     * never be released.
     */
    thisPtrWillGoToNil = nil;
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0</span>), ^ {
    NSCAssert(![NSThread isMainThread], @<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"Must be run on background thread"</span>);
    /**
     * If, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">as</span> mentioned <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">in</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">the</span> comment <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">above</span>, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">the</span> synchronized lock <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">is</span> never
     * released, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">then</span> we expect <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">to</span> wait forever <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">below</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">as</span> we <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">try</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">to</span> acquire
     * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">the</span> lock associated <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">with</span> `<span class="hljs-type" style="box-sizing: border-box;">number</span>`.
     *
     * This doesn't happen, so we conclude <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">that</span> `@synchronized` must deal
     * <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">with</span> this correctly.
     */
    @synchronized (<span class="hljs-type" style="box-sizing: border-box;">number</span>) {
        NSLog(@<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"This line does indeed get printed to stdout"</span>);
    }
});</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li><li style="box-sizing: border-box; padding: 0px 5px;">20</li><li style="box-sizing: border-box; padding: 0px 5px;">21</li><li style="box-sizing: border-box; padding: 0px 5px;">22</li><li style="box-sizing: border-box; padding: 0px 5px;">23</li><li style="box-sizing: border-box; padding: 0px 5px;">24</li><li style="box-sizing: border-box; padding: 0px 5px;">25</li></ul>

當我們執行上面的代碼時,那行代碼確實打印到控制檯了!所以 Objective-C 很好地處理了這種情形。我打賭是編譯器做了類似下面的事情來解決這事兒的。

<code class="hljs objectivec has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">NSString</span> *test = @<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"test"</span>;
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">id</span> synchronizeTarget = (<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">id</span>)test;
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">@try</span> {
    objc_sync_enter(synchronizeTarget);
    test = <span class="hljs-literal" style="color: rgb(0, 102, 102); box-sizing: border-box;">nil</span>;
} <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">@finally</span> {
    objc_sync_exit(synchronizeTarget);   
}</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li></ul>

用這種方式實現的話,傳遞給 objc_sync_enter 和 objc_sync_exit 總是相同的對象。他們在傳入 nil 時都是空操作。這帶來了個棘手的 debug 場景:如果你向 @synchronized 傳遞 nil,那麼你就不會得到任何鎖而且你的代碼將不會是線程安全的!如果你想知道爲什麼你正收到出乎意料的競態(race),確保你沒向你的 @synchronized 傳入 nil。你可以在 objc_sync_nil 上設置一個符號斷點來達到此目的。objc_sync_nil 是一個空方法,當 objc_sync_enter 函數被傳入 nil 時會被調用,折讓 debug 更容易些。

譯者注:下面是 objc_sync_enter 的源碼,主要邏輯很容易看懂,加深理解 objc_sync_nil:

<code class="hljs haskell has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-title" style="box-sizing: border-box;">int</span> objc_sync_enter(id obj)
{
    int result = <span class="hljs-type" style="box-sizing: border-box; color: rgb(102, 0, 102);">OBJC_SYNC_SUCCESS</span>;
    <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">if</span> (obj) {
        <span class="hljs-type" style="box-sizing: border-box; color: rgb(102, 0, 102);">SyncData</span>* <span class="hljs-typedef" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">data</span> = id2data<span class="hljs-container" style="box-sizing: border-box;">(<span class="hljs-title" style="box-sizing: border-box;">obj</span>, <span class="hljs-type" style="box-sizing: border-box; color: rgb(102, 0, 102);">ACQUIRE</span>)</span>;</span>
        require_action_string(<span class="hljs-typedef" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">data</span> != <span class="hljs-type" style="box-sizing: border-box; color: rgb(102, 0, 102);">NULL</span>, done, result = <span class="hljs-type" style="box-sizing: border-box; color: rgb(102, 0, 102);">OBJC_SYNC_NOT_INITIALIZED</span>, "id2data failed");</span>
        result = recursive_mutex_lock(&<span class="hljs-typedef" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">data</span>->mutex);</span>
        require_noerr_string(result, done, <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"mutex_lock failed"</span>);
    } <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">else</span> {
        // @synchronized(nil) does nothing
        <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">if</span> (<span class="hljs-type" style="box-sizing: border-box; color: rgb(102, 0, 102);">DebugNilSync</span>) {
            _objc_inform(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug"</span>);
        }
        objc_sync_nil();
    }
<span class="hljs-title" style="box-sizing: border-box;">done</span>: 
    return result;
}</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li></ul>

這回答了我眼下的問題。

你調用 sychronized 的每個對象,Objective-C runtime 都會爲其分配一個遞歸鎖並存儲在哈希表中。

如果在 sychronized 內部對象被釋放或被設爲 nil 看起來都 OK。不過這沒在文檔中說明,所以我不會再生產代碼中依賴這條。

注意不要向你的 sychronized block 傳入 nil!這將會從代碼中移走線程安全。你可以通過在 objc_sync_nil 上加斷點來查看是否發生了這樣的事情。

研究的下一步將是研究下 “synchronized block” 輸出的彙編,看看它是否跟我上面的例子相似。我打賭 @synchronized block 的彙編輸出不會跟任何我們設計的 Objective-C 代碼相同,上面的代碼充其量是 @synchronized 的工作模型。你能想到更好的模型麼?我的模型在哪些情形下會有瑕疵麼?告訴我吧!

轉自:http://www.cocoachina.com/ios/20151103/14007.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章