轉自:Hexo博客(@國內iOS第十二人 )
毫不誇張的說,80%的程序員對於多線程的理解都是淺陋和錯誤的。就拿我從事的iOS行業來說,雖然很多程序員可以對異步、GCD等等與線程相關的概念說的天花亂墜。但是實質上深挖本質的話,大多數人並不能很好的區分Race Condition,Atomic,Immutable對象在線程安全中真正起到的作用。
所以今天就以這篇文章來談談我所理解的線程安全。
首先就允許我從Immutable來開始整篇話題吧。
Swift中的Immutable
用過Swift的人都知道,Swift相較於Objective-C有一個比較明顯的改動就是將結構體(Struct)和類型(Class)進行了分離。從某種方面來說,Swift將值類型和引用類型進行了明顯的區分。為什麼要這麼做?
避免了引用類型在被作為參數傳遞後被他人持有後修改,從而引發比較難以排查的問題。
在某些程度上提供了一定的線程安全(因為多線程本身的問題很大程序上出在寫修改的不確定性)。而Immutable 數據的好處在於一旦創建結束就無法修改,因此相當於任一一個線程在使用它的過程中僅僅是使用了讀的功能。
看到這,很多人開始歡呼了(嘲諷下WWDC那些“托”一般的粉絲,哈哈),覺得線程安全的問題迎刃而解了。
但事實上,我想說的是使用Immutable不直接等同於線程安全,不然在使用NSArray,NSDictionary等等Immutable對象之後,為啥還會有那麼多奇怪的bug出現?
指針與對象
有些朋友會問,Immutable都將一個對象變為不可變的“固態”了,為什麼還是不安全呢,在各個線程間傳遞的只是一份只讀文件啊。
是的,對於一個Immutable的對象來說,它自身是不可變了。但是在我們的程序裡,我們總是需要有“東西”去指向我們的對象的吧,那這個“東西”是什麼?指向對象的指針。
指針想必大家都不會陌生。對於指針來說,其實它本質也是一種對象,我們更改指針的指向的時候,實質上就是對於指針的一種賦值。所以想象這樣一種場景,當你用一個指針指向一個Immutable對象的時候,在多線程更改的時候,你覺得你的指針修改是線程安全的嗎?這也就是為什麼有些人碰到一些跟NSArray這種Immutable對象的在多線程出現奇怪bug的時候會顯得一臉懵逼。
舉例:
// Thread A 其中immutableArrayA count 7 self.xxx = self.immutableArrayA; // Thread B 其中immutableArrayB count 4 self.xxx = self.immutableArrayB // main Thread [self.xxx objectAtIndex:5]
上述這個代碼片段,絕對是存在線程的安全的隱患的。
鎖
既然想到了多線程對於指針(或者對象)的修改,我們很理所當然的就會想到用鎖。在現如今iOS博客泛濫的年代,大家都知道NSLock, OSSpinLock之類的可以用於短暫的Critical Section競態的鎖保護。
所以對於一些多線程中需要使用共享數據源並支持修改操作的時候,比如NSMutableArray添加一些object的時候,我們可以寫出如下代碼:
OSSpinLock(&_lock); [self.array addObject:@"hahah"]; OSSpinUnlock(&_lock);
乍一看,這個沒問題了,這個就是最基本的寫保護鎖。如果有多個代碼同時嘗試添加進入self.array,是會通過鎖搶占的方式一個一個的方式的添加。
但是,這個東西有啥卵用嗎?原子鎖只能解決Race Condition的問題,但是它並不能解決任何你代碼中需要有時序保證的邏輯。
比如如下這段代碼:
if (self.xxx) { [self.dict setObject:@"ah" forKey:self.xxx]; }
大家第一眼看到這樣的代碼,是不是會認為是正確的?因為在設置key的時候已經提前進行了self.xxx為非nil的判斷,只有非nil得情況下才會執行後續的指令。但是,如上代碼只有在單線程的前提下才是正確的。
假設我們將上述代碼目前執行的線程為Thread A,當我們執行完if (self.xxx)的語句之後,此時CPU將執行權切換給了Thread B,而這個時候Thread B中調用了一句self.xxx = nil。
嘿嘿,後果如何,想必我不用多說了吧。
那對於這種問題,我們有沒有比較好的解決方案呢?答案是存在的,就是使用局部變量。
針對上述代碼,我們進行如下修改:
__strong id val = self.xxx; if (val) { [self.dict setObject:@"ah" forKey:val]; }
這樣,無論多少線程嘗試對self.xxx進行修改,本質上的val都會保持現有的狀態,符合非nil的判斷。
Objective-C的Property Setter多線程並發bug
最後我們回到經常使用的Objective-C來談談現實生活中經常出現的問題。相信各位對於Property的Setter概念都不陌生,self.xxx = @"kks"其實就是調用了xxx的setter方法。而Setter方法本質上就是如下這樣一段代碼邏輯:
- (void)setXxx:(NSString *)newXXX { if (newXXX != _xxx) { [newXXX retain]; [_xxx release]; _userName = newXXX; } }
比如Thread A 和 B同時對self.xxx進行了賦值,當兩者都越過了if (newXXX != _xxx)的判斷的時候,就會產生[_xxx release]執行了兩次,造成過度釋放的crash危險。
有人說,呵呵,你這是MRC時代的寫法,我用了ARC,沒問題了吧。
ok,那讓我們來看看ARC時代是怎麼處理的,對於ARC中不復寫Setter的屬性(我相信是絕大多數情況),Objective-C的底層源碼是這麼處理的。
static inline void reallySetProperty(id self, SEL _cmd, id newValue,
ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy) { id oldValue; // 計算結構體中的偏移量 id *slot = (id*) ((char*)self + offset); if (copy) { newValue = [newValue copyWithZone:NULL]; } else if (mutableCopy) { newValue = [newValue mutableCopyWithZone:NULL]; } else { // 某些程度的優化 if (*slot == newValue) return; newValue = objc_retain(newValue); } // 危險區 if (!atomic) { // 第一步 oldValue = *slot; // 第二步 *slot = newValue; } else { spin_lock_t *slotlock = &PropertyLocks[GOODHASH(slot)]; _spin_lock(slotlock); oldValue = *slot; *slot = newValue; _spin_unlock(slotlock); } objc_release(oldValue); }
由於我們一般聲明的對象都是nonatomic,所以邏輯會走到上述注釋危險區處。還是設想一下多線程對一個屬性同時設置的情況,我們首先在線程A處獲取到了執行第一步代碼後的oldValue,然後此時線程切換到了B,B也獲得了第一步後的oldValue,所以此時就有兩處持有oldValue。然後無論是線程A或者線程B執行到最後都會執行objc_release(oldValue);。
於是,重復釋放的場景就出現了,crash在向你招手哦!
如果不相信的話,可以嘗試如下這個小例子:
for (int i = 0; i 10000; i++) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ self.data = [[NSMutableData alloc] init]; }); }
相信你很容易就能看到如下錯誤log:error for object: pointer being freed was not allocated。
結語
說了這麼多,本質上線程安全是個一直存在並且相對來說是個比較困難的問題,沒有絕對的銀彈。用了Immutable不代表可以完全拋棄鎖,用了鎖也不代表高枕無憂了。希望這篇文章能夠幫助大家更深入的思考下相關的問題,不要見到線程安全相關的問題就直接回答加鎖、使用Immutable數據之類的。
當然,其實Stick To GCD (dispatch_barrier)是最好的解決方案。