關(guān)于@synchronized 比你想知道的還多

作者:Ryan Kaplan 譯者:徐嘉宏

原文地址:More than you want to know about @synchronized

如果你曾經(jīng)使用Objective-C做過并發(fā)編程痊班,那你肯定見過@synchronized這個結(jié)構(gòu)碳褒。@synchronized這個結(jié)構(gòu)發(fā)揮了和鎖一樣的作用:它避免了多個線程同時執(zhí)行同一段代碼哥攘。和使用NSLock進(jìn)行創(chuàng)建鎖、加鎖腰耙、解鎖相比,在某些情況下@synchronized會更方便迂猴、更易讀滓技。

如果你從來沒有使用過@synchronized,具體如何使用可以參考下面的實例冯吓。本文的將圍繞我對@synchronized的原理的探究進(jìn)行講述倘待。

使用@synchronized的例子

假如要用Objective-C實現(xiàn)一個線程安全的隊列疮跑,我們大概會這樣寫:

@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:方法失尖。許多線程會同時調(diào)用push:方法啊奄,然而[ _elements addObject:element];這行代碼也只能同時被一條線程訪問。這個流程應(yīng)該是這樣的:

  1. 線程A調(diào)用push:方法
  2. 線程B調(diào)用push:方法
  3. 線程B調(diào)用[_lock lock]掀潮,因為沒有其他線程持有這個鎖菇夸,因此線程B取得了這個鎖
  4. 線程A調(diào)用[_lock lock],但是此時這個鎖被線程B所持有仪吧,所以這個方法調(diào)用并沒有返回庄新,使線程A暫停了執(zhí)行
  5. 線程B添加了一個元素到_elements中,然后調(diào)用[ _lock unlock]方法薯鼠。此時择诈,線程A的[ _lock unlock]方法返回了,接著繼續(xù)執(zhí)行線程A的元素插入操作

使用@synchronized出皇,我們可以更簡潔明了的實現(xiàn)剛才的功能:

@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的代碼塊和前面例子中的[ _lock unlock]羞芍、[ _lock unlock]的作用相同作用效果。你可以把它理解成把self當(dāng)作一個NSLock來對self進(jìn)行加鎖郊艘。在運行{后的代碼前獲取鎖荷科,并在運行}后的其他代碼前釋放這個鎖。這非常的方便纱注,因為這意味著你永遠(yuǎn)不會忘了調(diào)用unlock

你也可以在任何Objective-C的對象上使用@synchronized畏浆。因此,同樣的我們也可以像下面的例子里一樣奈附,使用@synchronized(_elements)來代替@synchronized(self)全度,這兩者的效果是一致的。

回到我的探究上來

我對@synchronized的實現(xiàn)很好奇斥滤,于是我在谷歌搜索了它的一些細(xì)節(jié)将鸵。我找到了關(guān)于這個的一些回答 @synchronized是如何加鎖/解鎖的 在@synchronized中改變加鎖的對象 Apple的文檔,但沒有一個答案能給我足夠深入的解釋佑颇。傳入@synchronized的參數(shù)和這個鎖有什么關(guān)系顶掉?@synchronized是否持有它所加鎖的對象?如果傳入@synchronized代碼塊的對象在代碼塊里被析構(gòu)了或者被置為nil了會怎么樣挑胸?這些都是我想問的問題痒筒。在下文中,我會分享我的發(fā)現(xiàn)茬贵。

關(guān)于@synchronized的Apple的文檔中提到簿透,@synchronized代碼塊隱式地給被保護(hù)的代碼段添加了一個異常處理塊。這就是為什么在給某個對象保持同步的時候解藻,如果拋出了異常老充,鎖就會被釋放。

stackoverflow的一個回答中提到螟左,@synchronized塊會轉(zhuǎn)化成一對objc_sync_enterobjc_sync_exit的函數(shù)調(diào)用啡浊。我們并不知道這些函數(shù)都干了什么觅够,但是根據(jù)這個我們可以推斷,編譯器會像這樣轉(zhuǎn)化代碼:

@synchronized(obj) {
    // do work
}

轉(zhuǎn)換成大概像這樣的:

@try {
    objc_sync_enter(obj);
    // do work
} @finally {
    objc_sync_exit(obj);
}

具體什么是objc_sync_enterobject_sync_exit以及它們是如何實現(xiàn)的巷嚣,我們通過Command+點擊這兩個函數(shù)跳轉(zhuǎn)到了<objc/objc-sync.h>里喘先,這里有我們要找的兩個函數(shù):

// Begin synchronizing on 'obj'.
// Allocates recursive pthread_mutex associated with 'obj' if needed.
int objc_sync_enter(id obj)

// End synchronizing on 'obj'.
int objc_sync_exit(id obj)

在文件的最后,有一個蘋果工程師也是人的提示;)

// The wait/notify functions have never worked correctly and no longer exist.
int objc_sync_wait(id obj, long long milliSecondsMaxWait);
int objc_sync_notify(id obj);

總之廷粒,關(guān)于objc_sync_enter的文檔告訴了我們:@synchronized是基于一個遞歸鎖[1] 來傳遞一個對象的窘拯。什么時候分配內(nèi)存、如何分配內(nèi)存的坝茎?如何處理nil值树枫?幸運的是,Objective-C運行時是開源的景东,所以我們可以閱讀它的源碼找到答案砂轻。

你可以在這里查看所有objc-sync的源碼,但是我會領(lǐng)你在更高的層面通讀這些源碼斤吐。我們先從文件頂部的數(shù)據(jù)結(jié)構(gòu)看起搔涝。我會為你解釋下面的源碼因此你不必花時間來嘗試解讀這些代碼。

typedef struct SyncData {
    id object;
    recursive_mutex_t mutex;
    struct SyncData* nextData;
    int threadCount;
} SyncData;

typedef struct SyncList {
    SyncData *data;
    spinlock_t lock;
} SyncList;

static SyncList sDataLists[16];

首先和措,我們看到了結(jié)構(gòu)體struct SyncData的定義庄呈。這個結(jié)構(gòu)體包含了一個object(傳入@synchronized的對象)還有一個關(guān)聯(lián)著這個鎖以及被鎖對象的recursive_mutex_t。每個SyncData含有一個指向其他SyncData的指針nextData派阱,因此你可以認(rèn)為每個SyncData都是鏈表里的一個節(jié)點诬留。最后,每個SyncData含有一個threadCount來表示在使用或者等待鎖的線程的數(shù)量贫母。這很有用文兑,因為SyncData是被緩存的,當(dāng)threadCount == 0時腺劣,表示一個SyncData的實例能被復(fù)用绿贞。

接著,我們有了struct SyncList的定義橘原。正如我在前文中所提到的籍铁,你可以把一個SyncData當(dāng)作鏈表中的一個節(jié)點。每個SyncList結(jié)構(gòu)都有一個指向SyncData鏈表頭部的指針趾断,就像一個用于避免多線程并發(fā)的修改該鏈表的鎖一樣拒名。

這個代碼塊的最后一行之上是一個sDataLists的定義,這是一個SyncList結(jié)構(gòu)的數(shù)組芋酌。剛開始可能看起來不太像增显,但這個sDataList數(shù)組是一個哈希表(類似NSDictionary),用于把Objectice-C對象映射到他們對應(yīng)的鎖隔嫡。

當(dāng)你調(diào)用objc_sync_enter(obj)的時候甸怕,它通過一個記錄obj地址的哈希表來找到對應(yīng)的SyncData,然后對其加鎖腮恩。當(dāng)你調(diào)用objc_sync_exit的時候梢杭,它以同樣的方式找到對應(yīng)的SyncData并將其解鎖。

很好秸滴!現(xiàn)在我們知道了@synchronized是如何關(guān)聯(lián)一個鎖和那個被加同步鎖的對象武契,接下來,我會講講當(dāng)一個對象在@synchronized代碼塊中被析構(gòu)或者被置nil會發(fā)生什么荡含。

如果你看源碼的話咒唆,你會發(fā)現(xiàn)objc_sync_enter里面并沒有retains或者release。因此释液,它并不會持有傳入的對象全释,或者也有可能是因為它是在arc中編譯的。我們可以通過以下的代碼來進(jìn)行測試:

NSDate *test = [NSDate date];
// This should always be `1`
NSLog(@"%@", @([test retainCount]));

@synchronized (test) {

    // This will be `2` if `@synchronized` somehow
    // retains `test`
    NSLog(@"%@", @([test retainCount]));
}

對于每個的持有數(shù)误债,輸出總為1浸船。因此objc_sync_enter不會持有傳入的對象。這很有意思寝蹈。如果你需要同步的對象唄析構(gòu)了李命,然后可能另外一個新的對象被分配到了這個內(nèi)存地址上,很可能其他線程正嘗試同步那個有著和原對象有著相同地址的新的對象箫老。在這種情況下封字,其他線程會被阻塞直到當(dāng)前線程完成了自己的同步代碼塊。這似乎沒什么毛病耍鬓。這聽起來像這種實現(xiàn)是已被知曉的而且也沒什么問題阔籽。我并沒有看到其他更好的替代方案。

那如果這個對象在@synchronized代碼塊中被設(shè)成nil會怎樣呢牲蜀?再來看看我們的實現(xiàn):

NSString *test = @"test";
@try {
    // Allocates a lock for test and locks it
    objc_sync_enter(test);
    test = nil;
} @finally {
    // Passed `nil`, so the lock allocated in `objc_sync_enter`
    // above is never unlocked or deallocated
    objc_sync_exit(test);
}

調(diào)用objc_sync_enter的時候傳入test仿耽,調(diào)用objc_sync_exit的時候傳入nil。若objc_sync_exit傳入nil的時候什么都不做各薇,那么也不再會有人去釋放這個鎖项贺。這很糟糕。

Objective-C會那么輕易的被這種問題影響嗎峭判?下面的代碼把一個會被置nil的指針傳入@synchronized开缎。然后在后臺線程中往@synchronized中傳入一個指向同一對象的指針。如果在@synchronized中把一個對象置為nil讓這個鎖處于加鎖的狀態(tài)林螃,那么在第二個@synchronized中的代碼將永遠(yuǎn)不會被運行奕删。在控制臺中我們應(yīng)該什么都看不到。

NSNumber *number = @(1);
NSNumber *thisPtrWillGoToNil = number;

@synchronized (thisPtrWillGoToNil) {
    /**
     * Here we set the thing that we're synchronizing on to `nil`. If
     * implemented naively, the object would be passed to `objc_sync_enter`
     * and `nil` would be passed to `objc_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 with `number`.
     *
     * This doesn't happen, so we conclude that `@synchronized` must deal
     * with this correctly.
     */
    @synchronized (number) {
        NSLog(@"This line does indeed get printed to stdout");
    }

});

當(dāng)我們運行上述代碼時疗认,這行代碼卻的確被打印到控制臺上了完残!因此可以證明伏钠,Objective-C能很好的處理這種情況。我打賭這種情況是被編譯器處理過的谨设,大概如下:

NSString *test = @"test";
id synchronizeTarget = (id)test;
@try {
    objc_sync_enter(synchronizeTarget);
    test = nil;
} @finally {
    objc_sync_exit(synchronizeTarget);
}

有了這種實現(xiàn)熟掂,傳入objc_sync_enterobjc_sync_exit的對象總是相同的。當(dāng)傳入nil的時候他們什么都不會做扎拣。這引出了一個很棘手的debug場景:如果你往@synchronized里傳入nil赴肚,那么相當(dāng)于你并沒有進(jìn)行過加鎖操作,同時你的代碼將不再是線程安全的了二蓝!如果你被莫名其妙的問題困擾著誉券,那么先確保你沒有把nil傳入你的@synchronized代碼塊。你可以通過給objc_sync_nil設(shè)置一個符號斷點來檢查刊愚,objc_sync_nil是一個空方法踊跟,會在往objc_sync_enter傳入nil的時候調(diào)用,這會讓調(diào)試方便的多鸥诽。

現(xiàn)在琴锭,我的問題得到了回答。

  1. 對于每個加了同步的對象衙传,`Objective-C的運行時都會給其分配一個遞歸鎖决帖,并且保存在一個哈希表中。
  2. 一個被加了同步的對象被析構(gòu)或者被置為nil都是沒有問題的蓖捶。然而文檔中并沒有對此進(jìn)行什么說明地回,所以我也不會在任何實際的代碼中依賴這個。
  3. 注意不要往@synchronized代碼塊中傳入nil俊鱼!這會毀掉代碼的線程安全性刻像。通過往objc_sync_nil加入斷點你可以看到這種情況的發(fā)生。

探究的下一步是研究synchronized代碼塊轉(zhuǎn)成匯編的代碼并闲,看看是否和我前面的例子相似细睡。我打賭synchronized代碼塊轉(zhuǎn)換的匯編代碼不會和我們猜想的任何Objective-C代碼相似,上述的代碼例子只是@synchronized實現(xiàn)的模型而已帝火。你能想到更好的模型嗎溜徙?或者在我的這些例子中哪里有瑕疵?請告訴我犀填。

-完-

[1] 遞歸鎖蠢壹,是一種在已持有鎖的線程重復(fù)請求鎖卻不會發(fā)生死鎖的鎖。你可以在這里找到一個相關(guān)的例子九巡。有個很好用的類NSRecursiveLock图贸,它能實現(xiàn)這種效果,你可以試試。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末疏日,一起剝皮案震驚了整個濱河市偿洁,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌沟优,老刑警劉巖涕滋,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異净神,居然都是意外死亡,警方通過查閱死者的電腦和手機溉委,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進(jìn)店門鹃唯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人瓣喊,你說我怎么就攤上這事坡慌。” “怎么了藻三?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵洪橘,是天一觀的道長。 經(jīng)常有香客問我棵帽,道長熄求,這世上最難降的妖魔是什么匆绣? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任撵渡,我火速辦了婚禮绪穆,結(jié)果婚禮上重贺,老公的妹妹穿的比我還像新娘爬迟。我一直安慰自己恃慧,他們只是感情好部服,可當(dāng)我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布哟忍。 她就那樣靜靜地躺著铅搓,像睡著了一般瑟押。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上星掰,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天多望,我揣著相機與錄音,去河邊找鬼氢烘。 笑死便斥,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的威始。 我是一名探鬼主播枢纠,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了晋渺?” 一聲冷哼從身側(cè)響起镰绎,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎木西,沒想到半個月后畴栖,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡八千,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年吗讶,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片恋捆。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡照皆,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出沸停,到底是詐尸還是另有隱情膜毁,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布愤钾,位于F島的核電站瘟滨,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏能颁。R本人自食惡果不足惜杂瘸,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望伙菊。 院中可真熱鬧胧沫,春花似錦、人聲如沸占业。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽谦疾。三九已至南蹂,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間念恍,已是汗流浹背六剥。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留峰伙,地道東北人疗疟。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像瞳氓,于是被迫代替她去往敵國和親策彤。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 《招聘一個靠譜的 iOS》—參考答案(下) 說明:面試題來源是微博@我就叫Sunny怎么了的這篇博文:《招聘一個靠...
    韓發(fā)發(fā)吖閱讀 1,563評論 0 8
  • 1.寫一個NSString類的實現(xiàn) +(id)initWithCString:(c*****t char *)nu...
    韓七夏閱讀 3,767評論 2 37
  • 我寫稿仍然用紙和筆。雖然自己勉強有用電腦的條件店诗,但對屏幕的冷漠感心有余悸裹刮。再加拼音學(xué)的不通。面對閃亮的熒屏庞瘸,腦中一...
    黑土地_6345閱讀 562評論 5 3
  • 爺爺奶奶捧弃,我們都過的挺好的,你們不用擔(dān)心擦囊,雖然很少回去掃墓违霞,但是我們有我們的難處,等我們家過了這一難關(guān)后瞬场,我一定會...
    我要開始寫字了閱讀 192評論 0 0
  • 你對你生命的熱愛买鸽,并非是要你自己當(dāng)一個偉人,就像我對詩歌的熱愛泌类,并非需要多少人承認(rèn)癞谒,它是我心里的八卦新聞底燎,是我享受...
    6蟲閱讀 221評論 1 2