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

在軟件編程中杂拨,多線程是個繞不開的話題专普。多線程的使用,能夠提高程序的運行效率弹沽,但也帶來新的問題:如何保證線程安全檀夹?

在維基百科中線程安全的解釋是:指某個函數(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è)計

Reference

iOS多線程到底不安全在哪里翘单?

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末吨枉,一起剝皮案震驚了整個濱河市蹦渣,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌貌亭,老刑警劉巖柬唯,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異圃庭,居然都是意外死亡锄奢,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進店門剧腻,熙熙樓的掌柜王于貴愁眉苦臉地迎上來拘央,“玉大人,你說我怎么就攤上這事书在』椅埃” “怎么了?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵儒旬,是天一觀的道長栏账。 經(jīng)常有香客問我,道長义矛,這世上最難降的妖魔是什么发笔? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮凉翻,結(jié)果婚禮上了讨,老公的妹妹穿的比我還像新娘。我一直安慰自己制轰,他們只是感情好前计,可當我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著垃杖,像睡著了一般男杈。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上调俘,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天伶棒,我揣著相機與錄音,去河邊找鬼彩库。 笑死肤无,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的骇钦。 我是一名探鬼主播宛渐,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了窥翩?” 一聲冷哼從身側(cè)響起业岁,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎寇蚊,沒想到半個月后笔时,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡幔荒,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年糊闽,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片爹梁。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡右犹,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出姚垃,到底是詐尸還是另有隱情念链,我是刑警寧澤,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布积糯,位于F島的核電站掂墓,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏看成。R本人自食惡果不足惜君编,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望川慌。 院中可真熱鬧吃嘿,春花似錦、人聲如沸梦重。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽降瞳。三九已至,卻和暖如春蚓胸,著一層夾襖步出監(jiān)牢的瞬間挣饥,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工沛膳, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留扔枫,地道東北人。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓于置,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子八毯,可洞房花燭夜當晚...
    茶點故事閱讀 44,724評論 2 354

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

  • 下面是我自己收集整理的Java線程相關(guān)的面試題搓侄,可以用它來好好準備面試。 參考文檔:-《Java核心技術(shù) 卷一》-...
    阿呆變Geek閱讀 14,829評論 14 507
  • 接著上節(jié) mutex话速,本節(jié)主要介紹atomic的內(nèi)容讶踪,練習代碼地址。本文參考http://www.cplusplu...
    jorion閱讀 73,648評論 1 14
  • 一.線程安全性 線程安全是建立在對于對象狀態(tài)訪問操作進行管理泊交,特別是對共享的與可變的狀態(tài)的訪問 解釋下上面的話: ...
    黃大大吃不胖閱讀 842評論 0 3
  • 1 葉瑤在家入口的玄關(guān)處拖鞋的時候乳讥,還是察覺到了她的粉紅色棉拖和前兩天出差前擺放的地方不一樣。 她抿了抿嘴唇廓俭,把這...
    蕭洛zzy閱讀 2,209評論 3 6
  • 匆忙結(jié)束了一天工作的我云石,回到家,發(fā)現(xiàn)冷鍋冷灶研乒,一切都停停當當?shù)仂o默在那里汹忠,猛然想起自己加班,家人都回鄉(xiāng)下過周末兼避...
    坐忘mao閱讀 393評論 1 2