你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 從Immutable來談談對於線程安全的理解誤區

從Immutable來談談對於線程安全的理解誤區

編輯:IOS開發基礎

img-(48)600.jpg

轉自: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)是最好的解決方案。

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved