iOS刨根問底之@synchronized實現(xiàn)同步的原理

在利用 objc 進行多線程編程時常常遇到同步的問題台诗,這時用的最多的就是NSLock@synchronized@synchronizedNSLock使用起來會方便很多、可讀性較高项棠。

本文以一個例子開頭,請問下述代碼的輸出結(jié)果是什么:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block NSObject *obj = [NSObject new];
        dispatch_queue_t queue1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
        dispatch_queue_t queue2 = dispatch_queue_create("queue2", DISPATCH_QUEUE_CONCURRENT);
        dispatch_queue_t queue3 = dispatch_queue_create("queue3", DISPATCH_QUEUE_CONCURRENT);
                
        __block int a = 0;
        for (int i = 0; i < 10; i++) {
            dispatch_async(queue1, ^{
                @synchronized (obj) {
                    NSLog(@"queue1, a = %d", a++);
                    obj = nil;
                }
                // obj = [NSObject new];
            });
        }
    
        for (int i = 0; i < 10; i++) {
            dispatch_async(queue2, ^{
                @synchronized (nil) {
                    NSLog(@"queue2, a = %d", a++);
                }
            });
        }
        
        for (int i = 0; i < 10; i++) {
            dispatch_async(queue3, ^{
                @synchronized ([NSObject new]) {
                    NSLog(@"queue3, a = %d", a++);
                }
            });
        }
        
        // 延遲主線程退出挎峦,主線程退出子線程也會退出
        sleep(2);
    }
    return 0;
}

上述輸出結(jié)果是:三個循環(huán)中使用的鎖均無效(包括第一個循環(huán)中注釋無論是否打開)香追。下面就@synchronized的實現(xiàn)原理進行剖析。

NSObject *obj = [NSObject new];
@synchronized (obj) {
    NSLog(@"hello world.");
}

利用clang -rewrite-objc xxx將上述代碼轉(zhuǎn)化如下坦胶,代碼中已經(jīng)對關(guān)鍵內(nèi)容進行了注釋透典。這里說幾個點:

  1. 代碼中用到了很多的代碼塊({}結(jié)構(gòu)),是為了在執(zhí)行到}時顿苇,代碼塊中的對象釋放峭咒,觸發(fā)析構(gòu)函數(shù)的調(diào)用;
  2. 鎖入口函數(shù)objc_sync_enter纪岁,退出函數(shù)objc_sync_exit凑队;
  3. 如果鎖的釋放出現(xiàn)了異常,則會由catch塊捕獲幔翰,最終在FIN中拋出漩氨;
// 創(chuàng)建 obj 對象
NSObject *obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("new"));
{
    id _rethrow = 0;
    id _sync_obj = (id)obj;
    // 鎖的入口函數(shù)
    objc_sync_enter(_sync_obj);
    try {
        // 創(chuàng)建一個 _SYNC_EXIT 類型的對象,該對象在 try 代碼塊執(zhí)行完成后會調(diào)用其析構(gòu)函數(shù)釋放遗增,最終執(zhí)行 objc_sync_exit 釋放鎖叫惊。
        struct _SYNC_EXIT {
            _SYNC_EXIT(id arg) : sync_exit(arg) {}
            ~_SYNC_EXIT() {objc_sync_exit(sync_exit);}
            id sync_exit;
        } _sync_exit(_sync_obj);

        // NSLog(@"hello world.");
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_h5_359yk7215js43kb5v40w49sc0000gn_T_test_65e99a_mi_0);
    } catch (id e) {
        // 捕獲異常
        _rethrow = e;
    }
    {
        // 如果捕獲了異常,則會觸發(fā) _FIN 初始化的時候其 rethrow 變量有值贡定,并在對象釋放是調(diào)用析構(gòu)函數(shù)拋出異常赋访。
        struct _FIN {
            _FIN(id reth) : rethrow(reth) {}
            ~_FIN() { if (rethrow) objc_exception_throw(rethrow); }
            id rethrow;
        } _fin_force_rethow(_rethrow);
    }
}

objc_sync_enterobjc_sync_exit究竟做了什么?我們查看一下相關(guān)源碼,戳這里蚓耽。

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為空時,什么也沒做步悠。所以@synchronized(nil){}并不能達到鎖的效果签杈。我們發(fā)現(xiàn)上述兩個函數(shù)中都通過id2data獲取結(jié)構(gòu)體SyncData

如下代碼鼎兽,我列出了幾個關(guān)鍵結(jié)構(gòu):

// 鏈表結(jié)點
typedef struct SyncData {
    struct SyncData* nextData;  // next答姥,說明是個鏈表結(jié)構(gòu)
    DisguisedPtr<objc_object> object;    // synchronized 中的 obj 最終傳遞到這里
    int32_t threadCount;  // number of THREADS using this block
    recursive_mutex_t mutex;    // 互斥鎖
} SyncData;

// 鏈表
struct SyncList {
    SyncData *data;
    spinlock_t lock;    // 訪問該鏈表的鎖

    SyncList() : data(nil) { }
};

// Use multiple parallel lists to decrease contention among unrelated objects.
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
// 鏈表存儲的位置,StripedMap 用于存儲 void* -> T谚咬,即地址映射鹦付,具體內(nèi)容可以查看 StripedMap
static StripedMap<SyncList> sDataLists;

// 緩存時鏈表中的 node 結(jié)構(gòu)
typedef struct {
    SyncData *data;
    unsigned int lockCount;  // number of times THIS THREAD locked this block
} SyncCacheItem;

對于synchronized傳入的obj,有兩條鏈表進行存儲择卦,一條鏈表的node結(jié)構(gòu)是SyncData敲长,用于正常訪問用;另一條是SyncCacheItem秉继,從名字中可以看出是做緩存用祈噪,SyncCacheItem中有個SyncData類型的屬性。

SyncData是從函數(shù)id2data中獲取的尚辑,該函數(shù)內(nèi)容較多辑鲤,因為我們重點是關(guān)注synchronized傳入的對象是干什么用的,所以我簡單解釋下該函數(shù)的內(nèi)容:

  1. 查看緩存中是否有杠茬,判斷標準是緩存中SyncCacheItem.data.objectsynchronized傳入的obj的地址是否相等月褥;
  2. 如果緩存中沒有,則在sDataLists中查找澈蝙,判斷標準也是對象地址吓坚;
  3. 創(chuàng)建SyncData,并存儲在sDataLists中灯荧;
  4. 存儲到緩存中礁击;

id2data中第 3 步會創(chuàng)建SyncData對象,從中可以看到synchronized中傳入的obj最終存儲在SyncData->object中逗载。

SyncData **listp = &LIST_FOR_OBJ(object);
SyncData* result = NULL;
result = (SyncData*)calloc(sizeof(SyncData), 1);
result->object = (objc_object *)object;  // obj
result->threadCount = 1;
new (&result->mutex) recursive_mutex_t();
result->nextData = *listp;
// 添加到 sDataLists 鏈表中
*listp = result;

總結(jié):從@synchronized(){}的執(zhí)行流程我們可以得出如下結(jié)論:

  1. 不要傳遞nil對象哆窿,因為nil導(dǎo)致block執(zhí)行時沒有使用鎖;
  2. 兩次執(zhí)行synchronized傳入不同的對象厉斟,同步操作失效挚躯;
  3. 傳遞的obj對象,起作用的主要是對象地址擦秽,對象地址與使用的鎖一一對應(yīng)码荔;
  4. 如果在@synchronized(){}block中將obj置為nil漩勤,從代碼分析synchronized退出后,鎖并不會被釋放缩搅。那造成的結(jié)果是什么呢越败?要么下次訪問synchronized傳入的是新對象,要么下次傳入的是上次的obj(此時為nil)硼瓣。這兩種情況對應(yīng)上述結(jié)論1究飞、2,都會導(dǎo)致同步執(zhí)行失效堂鲤。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末亿傅,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子瘟栖,更是在濱河造成了極大的恐慌葵擎,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件慢宗,死亡現(xiàn)場離奇詭異坪蚁,居然都是意外死亡,警方通過查閱死者的電腦和手機镜沽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來贱田,“玉大人缅茉,你說我怎么就攤上這事∧写荩” “怎么了蔬墩?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長耗拓。 經(jīng)常有香客問我拇颅,道長,這世上最難降的妖魔是什么乔询? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任樟插,我火速辦了婚禮,結(jié)果婚禮上竿刁,老公的妹妹穿的比我還像新娘黄锤。我一直安慰自己,他們只是感情好食拜,可當我...
    茶點故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布鸵熟。 她就那樣靜靜地躺著,像睡著了一般负甸。 火紅的嫁衣襯著肌膚如雪流强。 梳的紋絲不亂的頭發(fā)上痹届,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天,我揣著相機與錄音打月,去河邊找鬼队腐。 笑死,一個胖子當著我的面吹牛僵控,可吹牛的內(nèi)容都是我干的香到。 我是一名探鬼主播,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼报破,長吁一口氣:“原來是場噩夢啊……” “哼悠就!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起充易,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤梗脾,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后盹靴,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體炸茧,經(jīng)...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年稿静,在試婚紗的時候發(fā)現(xiàn)自己被綠了梭冠。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,654評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡改备,死狀恐怖控漠,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情悬钳,我是刑警寧澤盐捷,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站默勾,受9級特大地震影響碉渡,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜母剥,卻給世界環(huán)境...
    茶點故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一滞诺、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧媳搪,春花似錦铭段、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至等限,卻和暖如春爸吮,著一層夾襖步出監(jiān)牢的瞬間芬膝,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工形娇, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留锰霜,地道東北人。 一個月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓桐早,卻偏偏與公主長得像癣缅,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子哄酝,可洞房花燭夜當晚...
    茶點故事閱讀 43,543評論 2 349