iOS鎖系列-@synchronized(self)

原文扔字,此文只為總結(jié)學(xué)習(xí)

因為原文一些內(nèi)容寫的不太準確,我按照我的理解做出了批注和補充温技。

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

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

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

用到 @synchronized 的例子

假設(shè)我們正在用 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 實例掷倔。這個類還有個 push: 方法眉孩,它先獲取鎖、然后向數(shù)組中插入元素勒葱、最終釋放鎖浪汪。可能會有許多線程同時調(diào)用 push: 方法凛虽,但是 [_elements addObject:element] 這行代碼在任何時候?qū)⒅粫谝粋€線程上運行死遭。步驟如下:

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

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

線程 B 調(diào)用 [_lock lock] - 因為當(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)更簡要地實現(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 block” 與 [_lock lock] 和 [_lock unlock] 效果相同荒椭。你可以把它當(dāng)成是鎖住 self谐鼎,仿佛 self 就是個 NSLock。鎖在左括號 { 后面的任何代碼運行之前被獲取到趣惠,在右括號 } 后面的任何代碼運行之前被釋放掉该面。這爽就爽在媽媽再也不用擔(dān)心我忘記調(diào)用 unlock 了!

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

回到研究上來

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

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

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

@synchronized(obj) {
    // do work
}

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

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

objc_sync_enter 和 objc_sync_exit 是什么鬼酒来?它們是如何實現(xiàn)的卢未?在 Xcode 中按住 Command 鍵單擊它們,然后進到了堰汉,里面有我們感興趣的這兩個函數(shù):


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

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

// 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失息?幸運的是 Objective-C runtime 是開源的,所以我們可以馬上閱讀源碼并找到答案档址!

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

你可以在這里找到 objc-sync 的全部源碼,但我要帶著你看源碼尼摹,讓你屌的飛起见芹。我們先從文件頂部的數(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;
// 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ù)量堡距。它很有用處甲锡,因為 SyncData 結(jié)構(gòu)體會被緩存兆蕉,threadCount==0 就暗示了這個 SyncData 實例可以被復(fù)用。

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

上面代碼塊的最后一行是 sDataLists 的聲明 - 一個 SyncList 結(jié)構(gòu)體數(shù)組,大小為16呆奕。通過定義的一個哈希算法將傳入對象映射到數(shù)組上的一個下標养晋。值得注意的是這個哈希算法設(shè)計的很巧妙,是將對象指針在內(nèi)存的地址轉(zhuǎn)化為無符號整型并右移五位梁钾,再跟 0xF 做按位與運算绳泉,這樣結(jié)果不會超出數(shù)組大小。 LOCK_FOR_OBJ(obj) 和 LIST_FOR_OBJ(obj) 這倆宏就更好理解了姆泻,先是哈希出對象的數(shù)組下標零酪,然后取出數(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 并將其解鎖。

譯者注:上面的源碼和幾段解釋有些原文解釋不清和疏漏的地方瓣赂,我看了源碼后按照自己的理解進行了補充和修正榆骚。

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

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


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

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

假如對象在 “synchronized block” 中被設(shè)成 nil 呢罗侯?我們回顧下我們“拿衣服(naive)”的實現(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);   
}

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 中的代碼將永遠不會執(zhí)行怎披。我們將不會在控制臺中看見任何東西打印出來。




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)我們執(zhí)行上面的代碼時瓶摆,那行代碼確實打印到控制臺了凉逛!所以 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_enter 和 objc_sync_exit 總是相同的對象。他們在傳入 nil 時都是空操作蝌借。這帶來了個棘手的 debug 場景:如果你向 @synchronized 傳遞 nil昔瞧,那么你就不會得到任何鎖而且你的代碼將不會是線程安全的!如果你想知道為什么你正收到出乎意料的競態(tài)(race)菩佑,確保你沒向你的 @synchronized 傳入 nil自晰。你可以在 objc_sync_nil 上設(shè)置一個符號斷點來達到此目的。objc_sync_nil 是一個空方法稍坯,當(dāng) objc_sync_enter 函數(shù)被傳入 nil 時會被調(diào)用酬荞,折讓 debug 更容易些。

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


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 上加斷點來查看是否發(fā)生了這樣的事情蛙埂。

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市盼理,隨后出現(xiàn)的幾起案子谈山,更是在濱河造成了極大的恐慌,老刑警劉巖宏怔,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件奏路,死亡現(xiàn)場離奇詭異,居然都是意外死亡臊诊,警方通過查閱死者的電腦和手機鸽粉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來抓艳,“玉大人触机,你說我怎么就攤上這事$杌颍” “怎么了儡首?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長偏友。 經(jīng)常有香客問我蔬胯,道長,這世上最難降的妖魔是什么位他? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任氛濒,我火速辦了婚禮,結(jié)果婚禮上鹅髓,老公的妹妹穿的比我還像新娘舞竿。我一直安慰自己,他們只是感情好窿冯,可當(dāng)我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布骗奖。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪重归。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天厦凤,我揣著相機與錄音鼻吮,去河邊找鬼。 笑死较鼓,一個胖子當(dāng)著我的面吹牛椎木,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播博烂,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼香椎,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了禽篱?” 一聲冷哼從身側(cè)響起畜伐,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎躺率,沒想到半個月后玛界,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡悼吱,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年慎框,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片后添。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡笨枯,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出遇西,到底是詐尸還是另有隱情馅精,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布努溃,位于F島的核電站硫嘶,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏梧税。R本人自食惡果不足惜沦疾,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望第队。 院中可真熱鬧哮塞,春花似錦、人聲如沸凳谦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽尸执。三九已至家凯,卻和暖如春缓醋,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背绊诲。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工送粱, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人掂之。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓抗俄,卻偏偏與公主長得像,于是被迫代替她去往敵國和親世舰。 傳聞我的和親對象是個殘疾皇子动雹,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,577評論 2 353

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