IOS多線程 - @synchronized

本文首發於 我的博客git

在IOS開發中,同步鎖相信你們都使用過,即 @synchronized ,這篇文章向你們介紹一些 @synchronized的原理和使用。github

@synchronized 原理

@synchronized 是IOS多線程同步中性能最差的:數組

倒是使用起來最方便的一個,一般咱們這麼用:sass

 @synchronized (self) {
        // code
    }
複製代碼

爲了瞭解其底層是如何 實現的,咱們在測試工程的ViewController.m中寫了一段代碼安全

static NSString *token = @"synchronized-token";
- (void)viewDidLoad {
    [super viewDidLoad];
    @synchronized (token) {
        // code
        NSLog(@"haha");
    }
}
複製代碼

點擊Xcode--> Debug -->Debug Workflow --> Always Show Disassembly顯示彙編,打上斷點啓動真機,就看到了以下代碼:markdown

上圖中的全部方法的調用我都圈出來了,ARC幫咱們自動插入了retainrelease,並且還找到了NSLog的方法,那麼包含NSLog的正是 objc_sync_enter objc_sync_exit 咱們猜想這個應該就是 @synchronized 的具體實現,因此咱們來到 Objective-c源碼 處查找,很快咱們發現了這兩個函數的實現:多線程

// Begin synchronizing on 'obj'. 
// Allocates recursive mutex associated with 'obj' if needed.
// Returns OBJC_SYNC_SUCCESS once lock is acquired.  
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;
}


// End synchronizing on 'obj'. 
// Returns OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
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;
}
複製代碼

從官方代碼和註釋中咱們能夠得出:async

  • 使用@synchronized 會建立一個遞歸(recursive)互斥(mutex)的鎖與 obj參數進行關聯。
  • @synchronized(nil)does nothing

遞歸鎖其實就是爲了方便同步鎖的嵌套使用而不會出現死鎖的狀況:函數

static NSString *token = @"synchronized-token";
static NSString *token1 = @"synchronized-token1";
- (void)viewDidLoad {
    [super viewDidLoad];
    @synchronized (token) {
        NSLog(@"haha");
        @synchronized (token1) {
            NSLog(@"didi");
        }
    }
}
複製代碼

@synchronized(nil) 不起任何做用,說明咱們要注意傳入obj 的生命週期,由於當obj被釋放這個地方就起不到加鎖的做用,有同窗可能注意到我這爲何沒有用self 做爲obj傳遞參數,就是爲了不token被多個地方持有修改,一旦出現nil,可能就會出現線程安全問題,這塊我會在後面去驗證。oop

obj 是如何保存的

咱們的@synchronized是針對傳入參數obj作綁定的,那麼內部obj到底是幹嗎用的,並且咱們知道@synchronizedobject-c全局均可以使用,那麼@synchronized是如何區分不一樣的obj進行一一對應的,帶着這些問題,咱們看看底層對obj到底是如何處理的。

首先咱們看到底層是這樣處理傳入的對象 obj 的:

SyncData* data = id2data(obj, ACQUIRE);
複製代碼

內部都會將 obj 轉化成相應的 SyncData 類型的對象,而後 id2data 內部是下面這樣取的:

SyncData **listp = &LIST_FOR_OBJ(object);
複製代碼

看看LIST_FOR_OBJ 是如何操做obj的(下面代碼留下關鍵部分,具體細節請自行前往源碼查看):

 1// obj傳入sDataLists
2#define LIST_FOR_OBJ(obj) sDataLists[obj].data
3
4// 哈希表結構,內部存SyncList
5static StripedMap<SyncList> sDataLists;
6
7// SyncList結構體,內部data就是SyncData
8struct SyncList {
9    SyncData *data;
10    spinlock_t lock;
11    constexpr SyncList() : data(nil), lock(fork_unsafe_lock{ }
12};
13
14// 哈希表結構
15class StripedMap {
16    enum { StripeCount = 64 };
17
18    struct PaddedT {
19        value alignas(CacheLineSize);
20    };
21
22    PaddedT array[StripeCount];
23
24    // 哈希函數
25    static unsigned int indexForPointer(const void *p{
26        uintptr_t addr = reinterpret_cast<uintptr_t>(p);
27        return ((addr >> 4) ^ (addr >> 9)) % StripeCount; 
28
29    }
30
31 public:
32   // 此處的p就是上面的obj,也就是obj執行上面的哈希函數對應到數組的index
33    T& operator[] (const void *p) { 
34        return array[indexForPointer(p)].value
35    }
複製代碼

從上述代碼看出總體StripedMap是一個哈希表結構,表外層是一個數組,數組裏的每一個位置存儲一個相似鏈表的結構(SyncList),SyncData 存儲的位置具體依賴第25行處的哈希函數,如圖:

obj1 處,通過哈希函數計算得出索引2,起初咱們要順着上面的 A 線對List進行查找,沒找到,將當前的obj插入到最前面,也是爲了更快的找到當前使用的對象而這麼設計。

    // Allocate a new SyncData and add to list.
    // XXX allocating memory with a global lock held is bad practice,
    // might be worth releasing the lock, allocating, and searching again.
    // But since we never free these guys we won't be stuck in allocation very often.
    posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData));
    result->object = (objc_object *)object;
    result->threadCount = 1;
    new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);
    result->nextData = *listp;
    *listp = result;
複製代碼

obj2 處就很少分析了,找到直接返回,進行加鎖解鎖處理。

慎用@synchronized(self)

@synchronized 中傳入object的內存地址,被用做key,經過必定的hash函數映射到一個系統全局維護的遞歸鎖中,因此不論傳入什麼類型的值,只要它有內存地址就能夠達到同步鎖的效果。

引用這篇文章開頭所說的例子:

一般咱們直接用@synchronized(self)

沒毛病,可是很粗糙,也確實存在問題。是否是他喵的被我說亂了,到底有沒有問題?

    @property (nonatomicstrongNSMutableArray *array;

    for (NSInteger i = 0; i < 20000; i ++) {
        dispatch_async(dispatch_get_global_queue(00), ^{
            self->_array = [NSMutableArray array];
        });
    }
複製代碼

這塊代碼咱們知道是有問題的,會直接Crash,緣由就在於多線程同時操做array,致使在某一個瞬間可能同時釋放了屢次,也就是野指針的問題。那麼咱們嘗試用今天的 @synchronized 來同步鎖一下:

    for (NSInteger i = 0; i < 20000; i ++) {
        dispatch_async(dispatch_get_global_queue(00), ^{
            @synchronized (self->_array) {
                self->_array = [NSMutableArray array];
            }
        });
    }
複製代碼

調試一下發現依然崩潰,@synchronized 不是加鎖嗎,怎麼還會Crash 呢?

原來咱們綁定的對象是array,而內部多線程對array的操做倒是頻繁的建立和release,當某個瞬間arry執行了release的時候就達成了咱們所說的 @synchronized(nil) ,上文已經分析了這個時候do nothing ! 因此能起到同步鎖的做用麼,很顯然不能,這就是崩潰的主要緣由。

因而可知@synchronized 在一些不斷的循環,遞歸的時候並不如人意,咱們惟獨要注意obj的參數惟一化,也就是與所要鎖的對象一一對應,這樣就避免了多個地方持有obj。

    static NSString *token = @"synchronized-token";
    @synchronized (token) {
        self.array1 = [NSMutableArray array];
    }
    --------------------------------------------------
    static NSString *another_token = @"another_token";
    @synchronized (another_token) {
        self.array2 = [NSMutableArray array];
    }
複製代碼

總結

雖然 @synchronized 使用起來很簡單,可是其內部的實現倒是很複雜,儘管其性能不好,適當的地方使用也無可厚非,無非就是注意對象的生命週期,以及內部的嵌套等。但願這篇文章能把@synchronized 講清楚,歡迎指正和溝通。

相關文章
相關標籤/搜索