深入理解Objective-C中的@Synchronized關鍵字

語言: CN / TW / HK

原文地址

在多執行緒程式設計中,執行緒之間共享資源時容易出現數據競爭的問題,導致程式出現不可預期的結果。為了避免這種情況,我們需要採用一些同步機制來保證執行緒之間的安全協作。 @synchronized指令是Objective-C中一種常用的同步機制。

@synchronized指令是Objective-C中一種非常簡單方便的建立鎖的方式。相比於其他鎖,它的語法更加簡單,只需要使用任意一個Objective-C物件作為鎖標記即可。

- (void)myMethod:(id)anObj { @synchronized(anObj) { // Everything between the braces is protected by the @synchronized directive. } }

@synchronized指令中傳遞的物件是用於區分受保護程式碼塊的唯一識別符號。如果在兩個不同的執行緒中執行上述方法,分別為anObj引數傳遞不同的物件,那麼每個執行緒都會獲取自己的鎖並繼續處理,而不會被另一個執行緒阻塞。但是,如果在這兩種情況下都傳遞相同的物件,則其中一個執行緒會首先獲取鎖,另一個執行緒則會被阻塞,直到第一個執行緒完成操作。

@Synchronized的底層實現

通過clang檢視底層編譯程式碼可知, @Synchronized是通過objc_sync_enter和objc_sync_exit函式來實現鎖的獲取和釋放的,原始碼如下:

``` int objc_sync_enter(id obj) { int result = OBJC_SYNC_SUCCESS;

if (obj) {
    SyncData* data = id2data(obj, ACQUIRE);
    ASSERT(data);
    data->mutex.lock();
} 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();
}

return result;

}

int objc_sync_exit(id obj) { int result = OBJC_SYNC_SUCCESS;

if (obj) {
    SyncData* data = id2data(obj, RELEASE); 
    if (!data) {
        result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
    } else {
        bool okay = data->mutex.tryUnlock();
        if (!okay) {
            result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
        }
    }
} else {
    // @synchronized(nil) does nothing
}


return result;

} ```

  • 如果傳入的obj存在,則走加鎖流程;如果obj為nil,則什麼也不做。
  • objc_sync_exit和objc_sync_enter是對應的;objc_sync_exit方法就是解鎖,如果obj= nil則什麼也不做;

通過觀察原始碼可知,objc_sync_exit和objc_sync_enter裡的關鍵是從obj轉換到SyncData,然後通過SyncData中的mutex來對臨界區上鎖。SyncData結構體的定義如下:

typedef struct alignas(CacheLineSize) SyncData { struct SyncData* nextData; DisguisedPtr<objc_object> object; int32_t threadCount; // number of THREADS using this block recursive_mutex_t mutex; } SyncData;

  • mutex是遞迴鎖,這也是為什麼可以在 @Synchronized裡巢狀 @Synchronized的原因了。

從obj轉換到SyncData的具體實現如下:

截圖2023-03-16 16.10.19.png

這段程式碼實現了一個鎖的快取機制,目的是為了提高多執行緒訪問同一物件時的效率。當多個執行緒同時訪問同一物件時,每個執行緒需要獲取一個鎖,這會造成效能瓶頸。為了避免這個問題,快取機制會將已經獲取的鎖快取起來,以供下次使用。其大致流程如下:

1、首先檢查是否啟用了快速快取,如果啟用則在快速快取中查詢是否有與obj對應的SyncData物件。
2、如果在快速快取中找到了匹配的SyncData物件,則將syncLockCount加1,並返回結果。
3、如果沒有在快速快取中找到匹配的SyncData物件,則繼續線上程快取中查詢是否有與obj對應的鎖。
4、如果線上程快取中找到了匹配的鎖,則將對應鎖的計數加1,並將其返回結果。
5、如果沒有線上程快取中找到匹配的鎖,則在全域性的雜湊表中查詢是否有與obj對應的SyncData物件。
6、如果在全域性的雜湊表中找到了匹配的SyncData物件,則會進行多執行緒操作,將對應鎖的計數加1,並返回結果。
7、如果沒有在全域性的雜湊表中找到匹配的SyncData物件,則建立新物件,並將新物件新增到上述的快取中,以供下次使用。

badcase分析

```

import "ViewController.h"

@interface ViewController ()

@property (nonatomic, strong) NSMutableArray *testArray;

@end

@implementation ViewController

  • (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. self.testArray = @[].mutableCopy;

    for (NSUInteger i = 0; i < 5000; i++) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [self testThreadArray]; }); } }

  • (void)testThreadArray { @synchronized (self.testArray) { self.testArray = @[].mutableCopy; } }

@end ```

執行這段程式碼,會出現如下crash:

截圖2023-03-25 20.59.43.png

考慮這個場景,有三個執行緒A、B、C同時訪問一個非原子屬性self.testArray,初始值為p0。執行緒A和執行緒B由於訪問的self.testArray的值一致,產生了競爭,執行緒A獲取了鎖並將self.testArray的值重新設定為p1,然後釋放了鎖。此時執行緒C訪問self.testArray,發現其值為p1,沒有競爭,準備對其進行賦值操作。然而,此時執行緒B由於之前的鎖已經被釋放,進入程式碼塊,也準備對self.testArray進行賦值操作,這會導致兩個執行緒同時對非原子屬性self.testArray進行賦值操作,從而產生crash。