線程鎖

@synchronized 是遞歸鎖,類似NSRecursiveLock,遞歸調(diào)用不會引起死鎖,而NSLock是非遞歸鎖。
因?yàn)樵囊恍﹥?nèi)容寫的不太準(zhǔn)確,我按照我的理解做出了批注和補(bǔ)充。

如果你已經(jīng)使用 Objective-C 編寫過任何并發(fā)程序,那么想必是見過 @synchronized 這貨了。@synchronized 結(jié)構(gòu)所做的事情跟鎖(lock)類似:它防止不同的線程同時執(zhí)行同一段代碼。但在某些情況下,相比于使用 NSLock 創(chuàng)建鎖對象、加鎖和解鎖來說,@synchronized 用著更方便,可讀性更高。

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

如果你之前沒用過 @synchronized,接下來有個使用它的例子。這篇文章實(shí)質(zhì)上是談?wù)動嘘P(guān)我對 @synchronized 實(shí)現(xiàn)原理的一個簡短研究。

用到 @synchronized 的例子

假設(shè)我們正在用 Objective-C 實(shí)現(xiàn)一個線程安全的隊(duì)列,我們一開始可能會這么干:

|

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

|

@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: 方法,它先獲取鎖、然后向數(shù)組中插入元素、最終釋放鎖??赡軙性S多線程同時調(diào)用 push: 方法,但是 [_elements addObject:element] 這行代碼在任何時候?qū)⒅粫谝粋€線程上運(yùn)行。步驟如下:

線程 A 調(diào)用 push: 方法

線程 B 調(diào)用 push: 方法

線程 B 調(diào)用 [_lock lock] - 因?yàn)楫?dāng)前沒有其他線程持有鎖,線程 B 獲得了鎖

線程 A 調(diào)用 [_lock lock],但是鎖已經(jīng)被線程 B 占了所以方法調(diào)用并沒有返回-這會暫停線程 A 的執(zhí)行

線程 B 向 _elements 添加元素后調(diào)用 [_lock unlock]。當(dāng)這些發(fā)生時,線程 A 的 [_lock lock] 方法返回,并繼續(xù)將自己的元素插入 _elements。

我們可以用 @synchronized 結(jié)構(gòu)更簡要地實(shí)現(xiàn)這些:

|

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

|

@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 就是個 NSLock。鎖在左括號 { 后面的任何代碼運(yùn)行之前被獲取到,在右括號 } 后面的任何代碼運(yùn)行之前被釋放掉。這爽就爽在媽媽再也不用擔(dān)心我忘記調(diào)用 unlock 了!

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

回到研究上來

我對 @synchronized 的實(shí)現(xiàn)十分好奇并搜了一些它的細(xì)節(jié)。我找到了一些答案,但這些解釋都沒有達(dá)到我想要的深度。鎖是如何與你傳入 @synchronized 的對象關(guān)聯(lián)上的?@synchronized會保持(retain,增加引用計(jì)數(shù))被鎖住的對象么?假如你傳入 @synchronized 的對象在 @synchronized 的 block 里面被釋放或者被賦值為 nil 將會怎么樣?這些全都是我想回答的問題。而我這次的收獲,會要你好看。

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

SO 上的這篇帖子 說 @synchronized block 會變成 objc_sync_enter 和 objc_sync_exit 的成對兒調(diào)用。我們不知道這些函數(shù)是干啥的,但基于這些事實(shí)我們可以認(rèn)為編譯器將這樣的代碼:

|

1

2

3

|

@synchronized(obj) {

// do work

}

|

轉(zhuǎn)化成這樣的東東:

|

1

2

3

4

5

6

|

@``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)到了,里面有我們感興趣的這兩個函數(shù):

|

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

|

/**

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

|

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

|

1

2

3

4

5

6

7

|

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

|

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

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

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

你可以在這里找到 objc-sync 的全部源碼,但我要帶著你看源碼,讓你屌的飛起。我們先從文件頂部的數(shù)據(jù)結(jié)構(gòu)開始看。在代碼塊的下方我將立刻做出解釋,所以嘗試?yán)斫獯a時別花太長時間哦。

|

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

|

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

|

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

下面是 struct SyncList 的定義。正如我在上面提過,你可以把 SyncData 當(dāng)做是鏈表中的節(jié)點(diǎn)。每個 SyncList 結(jié)構(gòu)體都有個指向 SyncData 節(jié)點(diǎn)鏈表頭部的指針,也有一個用于防止多個線程對此列表做并發(fā)修改的鎖。

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

當(dāng)你調(diào)用 objc_sync_enter(obj) 時,它用 obj 內(nèi)存地址的哈希值查找合適的 SyncData,然后將其上鎖。當(dāng)你調(diào)用 objc_sync_exit(obj) 時,它查找合適的 SyncData 并將其解鎖。

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

噢耶!現(xiàn)在我們知道了 @synchronized 如何將一個鎖和你正在同步的對象關(guān)聯(lián)起來,我希望聊聊當(dāng)一個對象在 @synchronized block 當(dāng)中被釋放或設(shè)為 nil 時會發(fā)生什么。

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

|

1

2

3

4

5

6

7

8

|

NSDate *test = [NSDate date];

// This should always be1``

NSLog(@``"%@"``, @([test retainCount]));

@synchronized (test) {

// This will be2if@synchronizedsomehow

// retainstest``

NSLog(@``"%@"``, @([test retainCount]));

}

|

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

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

|

1

2

3

4

5

6

7

8

9

10

|

NSString *test = @``"test"``;

@``try {

// Allocates a lock for test and locks it

objc_sync_enter(test);

test = nil;

} @finally {

// Passednil, so the lock allocated inobjc_sync_enter``

// above is never unlocked or deallocated

objc_sync_exit(test);

}

|

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

如果 Objective-C 容易受這種情況的影響,我們知道么?下面的代碼調(diào)用 @synchronized 并在 @synchronized block 中將一個指針設(shè)為 nil。然后在后臺線程對指向同一個對象的指針調(diào)用 @synchronized。如果在 @synchronized block 中設(shè)置一個對象為 nil 會讓鎖死鎖,那么在第二個 @synchronized 中的代碼將永遠(yuǎn)不會執(zhí)行。我們將不會在控制臺中看見任何東西打印出來。

|

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

|

NSNumber *number = @(1);

NSNumber *thisPtrWillGoToNil = number;

@synchronized (thisPtrWillGoToNil) {

/**

* Here we set the thing that we're synchronizing on tonil. If

* implemented naively, the object would be passed toobjc_sync_enter``

* andnilwould be passed toobjc_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 withnumber.

*

* This doesn't happen, so we conclude that@synchronizedmust deal

* with this correctly.

*/

@synchronized (number) {

NSLog(@``"This line does indeed get printed to stdout"``);

}

});

|

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

|

1

2

3

4

5

6

7

8

|

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 總是相同的對象。他們在傳入 nil 時都是空操作。這帶來了個棘手的 debug 場景:如果你向 @synchronized 傳遞 nil,那么你就不會得到任何鎖而且你的代碼將不會是線程安全的!如果你想知道為什么你正收到出乎意料的競態(tài)(race),確保你沒向你的 @synchronized 傳入 nil。你可以在 objc_sync_nil 上設(shè)置一個符號斷點(diǎn)來達(dá)到此目的。objc_sync_nil 是一個空方法,當(dāng) objc_sync_enter 函數(shù)被傳入 nil 時會被調(diào)用,折讓 debug 更容易些。

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

|

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

|

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;

}

|

這回答了我眼下的問題。

你調(diào)用 sychronized 的每個對象,Objective-C runtime 都會為其分配一個遞歸鎖并存儲在哈希表中。

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

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

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 參考鏈接:http://smallbug-vip.iteye.com/blog/2275743 在多線程開發(fā)的過程...
    時之令閱讀 1,641評論 2 5
  • 原文,此文只為總結(jié)學(xué)習(xí) 因?yàn)樵囊恍﹥?nèi)容寫的不太準(zhǔn)確,我按照我的理解做出了批注和補(bǔ)充。 如果你已經(jīng)使用 Objec...
    lltree閱讀 3,075評論 2 11
  • 線程安全是怎么產(chǎn)生的 常見比如線程內(nèi)操作了一個線程外的非線程安全變量,這個時候一定要考慮線程安全和同步。 - (v...
    幽城88閱讀 764評論 0 0
  • demo下載 建議一邊看文章,一邊看代碼。 聲明:關(guān)于性能的分析是基于我的測試代碼來的,我也看到和網(wǎng)上很多測試結(jié)果...
    炸街程序猿閱讀 850評論 0 2
  • 讀完了格雷厄姆·格林(Graham Greene))大師寫的《命運(yùn)的內(nèi)核》,對這位21次諾貝爾文學(xué)獎提名的傳奇大師...
    白色石南閱讀 1,098評論 0 2

友情鏈接更多精彩內(nèi)容