在軟件編程中杂拨,多線程是個繞不開的話題专普。多線程的使用,能夠提高程序的運行效率弹沽,但也帶來新的問題:如何保證線程安全檀夹?
在維基百科中線程安全的解釋是:指某個函數(shù)筋粗、函數(shù)庫在多線程環(huán)境中被調(diào)用時,能夠正確地處理多個線程之間的共享變量炸渡,使程序功能正確完成娜亿。換句話說,就是某個變量在被某條線程訪問期間是“一致”的蚌堵。這個“一致”指的是這條線程從開始訪問這個變量到結(jié)束訪問這個變量期間买决,這個變量不會發(fā)生任何變化。
那么吼畏,保證某個變量的線程安全督赤,也就可以理解成保證某個變量在某個特定時間段內(nèi)是一致的。這個某個特定時間泻蚊,也就可以理解成為線程安全的原子性粒度躲舌,具體下面有介紹。
例子
具體到iOS上性雄,經(jīng)常能看到下面的代碼例子:
// 例子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關(guān)鍵字秒旋,并不一定能保證屬性的線程安全
约计。
線程安全的原子性粒度
那為什么用了atomic
關(guān)鍵字不能保證上述場景的property變量的線程安全?
atomic
關(guān)鍵字的作用其實就是對屬性的讀寫操作進行加鎖迁筛,換句話說就是對屬性的Setter/Getter操作加鎖病蛉。但atomic
關(guān)鍵字只能保證在同一時間段內(nèi),最多有且只有一條線程對當前關(guān)鍵字進行讀寫瑰煎。
例子1中self.num = self.num + 1;
包含了三個操作:通過Getter讀取num,對讀取的num進行加1俗孝,將加1后的結(jié)果寫回num酒甸。atomic
關(guān)鍵字能保證每一個操作都是原子的。但是赋铝,每個操作之間的間隙時間插勤,atomic
不能保證屬性不被其他線程訪問。在TheadA對num進行加1操作后革骨,此時CPU時間被分配給了Thread B农尖,Thread B有可能對num進行了修改,當CPU時間再次分配回Thread A的時候良哲,此時的num+1不一定是原來的num+1盛卡,此時Thread 將當前的num值修改成原來的的num+1的值,最后導(dǎo)致預(yù)期值跟實際值不一樣筑凫,這種場景就是多線程的線程不安全
滑沧。而且使用atomic
無法避免一個問題并村,如果多線程對屬性的訪問是直接通過Ivar來訪問, 不通過調(diào)用Getter/Setter來訪問的話滓技,atomic
沒有任何作用哩牍。
同樣,例子2也是一樣令漂,當執(zhí)行代碼self.stringA.length >= 10
時膝昆,假設(shè)stringA的值是“a very long string”,符合判斷條件叠必,此時線程切換到Thread A荚孵,Thread A將stringA修改成“string”。這時CPU時間再次分配給Thread B挠唆,此時Thread B會執(zhí)行[self.stringA substringWithRange:NSMakeRange(0, 10)]
处窥,但當前的stringA的值已經(jīng)被Thread A修改成了“string”,所以會字符串訪問越界玄组,直接crash滔驾。
例子1和例子2出現(xiàn)問題的原因在于雖然對字符串的每次讀寫都是安全的,但是并不能保證各個線程組合起來的操作是安全的俄讹,這就是一個線程安全的原子性粒度問題哆致。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];
對代碼進行加鎖后踪蹬,只有對加鎖代碼加鎖了的線程才能訪問加鎖代碼胞此,這樣就保證了加鎖代碼不會被其他線程執(zhí)行,從而從更大粒度上保證了線程安全跃捣。如果使用了鎖機制進行代碼級原子粒度的控制漱牵,就沒有必要再使用更小粒度的atomic
了。因為大粒度的原子性已經(jīng)能夠保障相關(guān)業(yè)務(wù)代碼的線程安全疚漆,如果再加多更小粒度的原子性控制酣胀,一來會多此一舉,二來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發(fā)生crash的原因是,stringA指向的內(nèi)存區(qū)域發(fā)生了變化涛碑,訪問時發(fā)生了越界精堕。但例子5中則不會有這種情況,因為例子5中使用了臨時變量immutableTempString蒲障,指向stringA未發(fā)生變化前的內(nèi)存空間歹篓,當stringA指向的內(nèi)存發(fā)生變化后,由于原來stringA指向的內(nèi)存被immutableTempString指向揉阎,所以暫時不會被系統(tǒng)回收庄撮。當[immutableTempString substringWithRange:NSMakeRange(0, 10)]
調(diào)用時,immutableTempString指向的還是原來的stringA的值毙籽,所以不會發(fā)生crash洞斯。這種方法的原理是,通過使用臨時變量來持有原來變動前的值坑赡,所有操作都對這個臨時變量指向的值進行操作烙如,而不是直接使用屬性指向的值,這樣的話能保證上下文情景下變量的值是一致的毅否,而且由于變量是臨時變量亚铁,所以只會對當前線程可見,對其他線程不可見螟加,從而在某種程度上保證了線程安全徘溢。
總結(jié)
在iOS中,不能簡單的認為只要加上atomic
關(guān)鍵字就能保證屬性的線程安全捆探。而在實際使用中然爆,由于業(yè)務(wù)代碼的復(fù)雜性,大部分情況下都會使用比atomic
更大粒度的鎖控制黍图。由于使用了更大粒度的鎖施蜜,從性能和必要性方面考慮,就不需要再使用atomic
了雌隅。在某些情況下,如果不能采用加鎖的做法缸沃,又要保證代碼不會發(fā)生crash恰起,可以使用臨時變量指向原值,保證一定程度的線程安全趾牧。
總而言之检盼,多線程的線程安全是個復(fù)雜的問題,最好的做法是盡量避免多線程的設(shè)計