怎樣作才能保證線程安全?

在軟件編程中,多線程是個繞不開的話題。多線程的使用,可以提升程序的運行效率,但也帶來新的問題:如何保證線程安全?ios

在維基百科中線程安全的解釋是:指某個函數、函數庫在多線程環境中被調用時,可以正確地處理多個線程之間的共享變量,使程序功能正確完成。換句話說,就是某個變量在被某條線程訪問期間是「一致」的。這個「一致」指的是這條線程從開始訪問這個變量到結束訪問這個變量期間,這個變量不會發生任何變化。編程

那麼,保證某個變量的線程安全,也就能夠理解成保證某個變量在某個特定時間段內是一致的。這個某個特定時間,也就能夠理解成爲線程安全的原子性粒度,具體下面有介紹。安全

例子

具體到iOS上,常常能看到下面的代碼例子:bash

// 例子1
@property (atomic, assign) int num;

// thread A
for (int i = 0; i < 10000; i++) {
    self.num = self.num + 1;
    NSLog(@"Thread A: %d\d ",self.num);
}

// thread B
for (int i = 0; i < 10000; i++) {
    self.num = self.num + 1;
    NSLog(@"Thread B: %d\d ",self.num);
}複製代碼
// 例子2
@property (atomic, strong) NSString   * stringA;

//thread A
for (int i = 0; i < 10000; i ++) {
    if (i % 2 == 0) {
        self.stringA = @"a very long string";
    }
    else {
        self.stringA = @"string";
    }
    NSLog(@"Thread A: %@\n", self.stringA);
}

//thread B
for (int i = 0; i < 10000; i ++) {
    if (self.stringA.length >= 10) {
        NSString* subStr = [self.stringA substringWithRange:NSMakeRange(0, 10)];
    }
    NSLog(@"Thread B: %@\n", self.stringA);
}複製代碼

例子A最後輸出不必定是20000,例子B有可能會crash。這兩個例子說明了一個問題:property加上atomic關鍵字,並不必定能保證屬性的線程安全多線程

線程安全的原子性粒度

那爲何用了atomic關鍵字不能保證上述場景的property變量的線程安全?函數

atomic關鍵字的做用其實就是對屬性的讀寫操做進行加鎖,換句話說就是對屬性的Setter/Getter操做加鎖。但atomic關鍵字只能保證在同一時間段內,最多有且只有一條線程對當前關鍵字進行讀寫。性能

例子1中self.num = self.num + 1;包含了三個操做:經過Getter讀取num,對讀取的num進行加1,將加1後的結果寫回num。atomic關鍵字能保證每個操做都是原子的。可是,每一個操做之間的間隙時間,atomic不能保證屬性不被其餘線程訪問。在TheadA對num進行加1操做後,此時CPU時間被分配給了Thread B,Thread B有可能對num進行了修改,當CPU時間再次分配回Thread A的時候,此時的num+1不必定是原來的num+1,此時Thread 將當前的num值修改爲原來的的num+1的值,最後致使預期值跟實際值不同,這種場景就是多線程的線程不安全。並且使用atomic沒法避免一個問題,若是多線程對屬性的訪問是直接經過Ivar來訪問, 不經過調用Getter/Setter來訪問的話,atomic沒有任何做用。atom

一樣,例子2也是同樣,當執行代碼self.stringA.length >= 10時,假設stringA的值是「a very long string」,符合判斷條件,此時線程切換到Thread A,Thread A將stringA修改爲「string」。這時CPU時間再次分配給Thread B,此時Thread B會執行[self.stringA substringWithRange:NSMakeRange(0, 10)],但當前的stringA的值已經被Thread A修改爲了「string」,因此會字符串訪問越界,直接crash。spa

例子1和例子2出現問題的緣由在於雖然對字符串的每次讀寫都是安全的,可是並不能保證各個線程組合起來的操做是安全的,這就是一個線程安全的原子性粒度問題。atomic的原子粒度是Getter/Setter,但對多行代碼的操做不能保證原子性。針對例子1和例子2的問題,更好的辦法是使用鎖機制。線程

// 例子3
// thread A
[_lock lock];
for (int i = 0; i < 10000; i++) {
    self.num = self.num + 1;
    NSLog(@"Thread A: %d\d ",self.num);
}
[_lock unlock];

// thread B
[_lock lock];
for (int i = 0; i < 10000; i++) {
    self.num = self.num + 1;
    NSLog(@"Thread B: %d\d ",self.num);
}
[_lock unlock];複製代碼
// 例子4
//thread A
[_lock lock];
for (int i = 0; i < 10000; i ++) {
    if (i % 2 == 0) {
        self.stringA = @"a very long string";
    }
    else {
        self.stringA = @"string";
    }
    NSLog(@"Thread A: %@\n", self.stringA);
}
[_lock unlock];

//thread B
[_lock lock];
for (int i = 0; i < 10000; i ++) {
    if (self.stringA.length >= 10) {
        NSString* subStr = [self.stringA substringWithRange:NSMakeRange(0, 10)];
    }
    NSLog(@"Thread B: %@\n", self.stringA);
}
[_lock unlock];複製代碼

對代碼進行加鎖後,只有對加鎖代碼加鎖了的線程才能訪問加鎖代碼,這樣就保證了加鎖代碼不會被其餘線程執行,從而從更大粒度上保證了線程安全。若是使用了鎖機制進行代碼級原子粒度的控制,就沒有必要再使用更小粒度的atomic了。由於大粒度的原子性已經可以保障相關業務代碼的線程安全,若是再加多更小粒度的原子性控制,一來會畫蛇添足,二來atomic是一種更小粒度的加鎖機制,會對性能有很多的影響,因此通常來講若是使用了更大粒度的原子性,就沒有必要使用更小粒度的原子性了,因此加鎖後的代碼中的屬性變量,沒有必要再使用atomic

不加鎖的小技巧

對於例子2,若是不加鎖,怎麼保證不會代碼不會crash?

// 例子5
for (int i = 0; i < 10000; i ++) {
    NSString *immutableTempString = self.stringA;
    if (immutableTempString.length >= 10) {
        NSString* subStr = [immutableTempString substringWithRange:NSMakeRange(0, 10)];
    }
}複製代碼

例子2發生crash的緣由是,stringA指向的內存區域發生了變化,訪問時發生了越界。但例子5中則不會有這種狀況,由於例子5中使用了臨時變量immutableTempString,指向stringA未發生變化前的內存空間,當stringA指向的內存發生變化後,因爲原來stringA指向的內存被immutableTempString指向,因此暫時不會被系統回收。當[immutableTempString substringWithRange:NSMakeRange(0, 10)]調用時,immutableTempString指向的仍是原來的stringA的值,因此不會發生crash。這種方法的原理是,經過使用臨時變量來持有原來變更前的值,全部操做都對這個臨時變量指向的值進行操做,而不是直接使用屬性指向的值,這樣的話能保證上下文情景下變量的值是一致的,並且因爲變量是臨時變量,因此只會對當前線程可見,對其餘線程不可見,從而在某種程度上保證了線程安全。

總結

在iOS中,不能簡單的認爲只要加上atomic關鍵字就能保證屬性的線程安全。而在實際使用中,因爲業務代碼的複雜性,大部分狀況下都會使用比atomic更大粒度的鎖控制。因爲使用了更大粒度的鎖,從性能和必要性方面考慮,就不須要再使用atomic了。在某些狀況下,若是不能採用加鎖的作法,又要保證代碼不會發生crash,可使用臨時變量指向原值,保證必定程度的線程安全。

總而言之,多線程的線程安全是個複雜的問題,最好的作法是儘可能避免多線程的設計

Reference

iOS多線程到底不安全在哪裏?

相關文章
相關標籤/搜索