你不知道的的 iOS 多線程

圖片來自網(wǎng)絡(luò)

程序員用有限的生命去追求無限的知識。

有言在先

首先我不是故意要做標(biāo)題黨的,也不是我要炒冷飯,我只是想換個姿勢看多線程,本文大部分內(nèi)容在分析如何造死鎖,奈何功力尚淺,然而再淺,也需要走出第一步。打開你的 Xcode 來驗(yàn)證這些死鎖吧。

多線程小知識

以下是實(shí)現(xiàn)多線程的三種方式:

  • NSThread
  • GCD
  • NSOperationQueue

關(guān)于具體使用的方法不再具體介紹,讓我們來看看他們不為人知的一面

1. 鎖的背后

NSLock是基于 POSIX threads 實(shí)現(xiàn)的,而 POSIX threads 中使用互斥量同步線程。

互斥量(或稱為互斥鎖)是 pthread 庫為解決這個問題提供的一個基本的機(jī)制。互斥量是一個鎖,它保證如下三件事情:

  • 原子性 - 鎖住一個互斥量是一個原子操作,表明操作系統(tǒng)保證如果在你已經(jīng)鎖了一個互斥量,那么在同一時刻就不會有其他線程能夠鎖住這個互斥量;

  • 奇異性 - 如果一個線程鎖住了一個互斥量,那么可以保證的是在該線程釋放這個鎖之前沒有其他線程可以鎖住這個互斥量;

  • 非忙等待 - 如果一個線程(線程1)嘗試去鎖住一個由線程2鎖住的鎖,線程1會掛起(suspend)并且不會消耗任何CPU資源,直到線程2釋放了這個鎖。這時,線程1會喚醒并繼續(xù)執(zhí)行,鎖住這個互斥量。

2. 關(guān)于生命周期

通過 [NSThread exit] 方法使線程退出 ,NSThread 是可以立即終止正在執(zhí)行的任務(wù)(可能會造成內(nèi)存泄露,這里不深究)。甚至你可以在主線程中執(zhí)行該操作,會使主線程也退出,app 無法再響應(yīng)事件。而 cancel 可以通過作為標(biāo)志位來達(dá)到類似目的,如果不做任何處理,仍然會繼續(xù)執(zhí)行。

GCD和NSOperationQueue可以取消隊列中未開始執(zhí)行的任務(wù),對于已經(jīng)開始執(zhí)行的任務(wù)就無能為力了。

實(shí)現(xiàn)方式\功能 線程生命周期 取消任務(wù)
NSThread 手動管理 立即停止執(zhí)行
GCD 自動管理 取消隊列中未執(zhí)行的任務(wù)
NSOperationQueue 自動管理 取消隊列中未執(zhí)行的任務(wù)

3. 并行與并發(fā)

看到很多文章里提到 并發(fā)隊列 ,這里有一個小陷阱,混淆了 并發(fā)并行 的概念。我們先來看看一下他們之間的區(qū)別:

并發(fā)與并行

從圖中可以看到,并行才是真正的多線程,而并發(fā)只是在多任務(wù)中切換。一般多核CPU可以并行執(zhí)行多個線程,而單核CPU實(shí)際上只有一個線程,多路復(fù)用達(dá)到接近同時執(zhí)行的效果。在 iOS 中 dispatch_async 和 globalQueue 從 Xcode 中線程使用情況來看,都達(dá)到了并行的效果。

4. 隊列與線程

隊列是保存以及管理任務(wù)的,將任務(wù)加到隊列中,任務(wù)會按照加入到隊列中先后順序依次執(zhí)行。如果是全局隊列和并發(fā)隊列,則系統(tǒng)會根據(jù)系統(tǒng)資源去創(chuàng)建新的線程去處理隊列中的任務(wù),線程的創(chuàng)建、維護(hù)和銷毀由操作系統(tǒng)管理,還有隊列本身是線程安全的。

使用 NSOperationQueue 實(shí)現(xiàn)多線程的時候是可以控制線程總數(shù)及線程依賴關(guān)系的,而 GCD 只能選擇并發(fā)或者串行隊列。

資源競爭

多線程同時執(zhí)行任務(wù)能提高程序的執(zhí)行效率和響應(yīng)時間,但是多線程不可避免地遇到同時操作同一資源的情況。前段時間看到的一個資源競爭的問題為例:

@property (nonatomic, strong) NSString *target; 
dispatch_queue_t queue = dispatch_queue_create("parallel", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 1000000 ; i++) { 
    dispatch_async(queue, ^{ 
        self.target = [NSString stringWithFormat:@"ksddkjalkjd%d",i]; 
    }); 
}

解決辦法:

  • @property (nonatomic, strong) NSString *target;nonatomic改成atomic
  • 將并發(fā)隊列 DISPATCH_QUEUE_CONCURRENT 改成串行隊列 DISPATCH_QUEUE_SERIAL
  • 異步執(zhí)行dispatch_async 改成同步執(zhí)行dispatch_sync
  • 賦值使用@synchronized 或者上鎖。

這些方法都是從避免同時訪問的角度來解決該問題,有更好的方法歡迎分享。

花樣死鎖

任何事情都有兩面性,就像多線程能提升效率的同時,也會造成資源競爭的問題。而鎖在保證多線程的數(shù)據(jù)安全的同時,粗心大意之下也容易發(fā)生問題,那就是 死鎖

1. NSOperationQueue

鑒于 NSOperationQueue 高度封裝,使用起來非常簡單,一般不會出什么幺蛾子,下面的案例展示了一個不好示范,通常我們通過控制 NSOperation 之間的從屬關(guān)系,來達(dá)到有序執(zhí)行任務(wù)的效果,但是如果互相從屬或者循環(huán)從屬都會造成所有任務(wù)無法開始。

 NSBlockOperation *blockOperation1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"lock 1 start");
        [NSThread sleepForTimeInterval:1];
        NSLog(@"lock 1 over");
    }];
    
    NSBlockOperation *blockOperation2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"lock 2 start");
        [NSThread sleepForTimeInterval:1];
        NSLog(@"lock 2 over");
    }];
    
    NSBlockOperation *blockOperation3 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"lock 3 start");
        [NSThread sleepForTimeInterval:1];
        NSLog(@"lock 3 over");
    }];
    
    // 循環(huán)從屬
    [blockOperation2 addDependency:blockOperation1];
    [blockOperation3 addDependency:blockOperation2];
    [blockOperation1 addDependency:blockOperation3]; // 循環(huán)的罪魁禍?zhǔn)?
    // 互相從屬
    //[blockOperation1 addDependency:blockOperation2];
    //[blockOperation2 addDependency:blockOperation1];

    [_operationQueue addOperation:blockOperation1];
    [_operationQueue addOperation:blockOperation2];
    [_operationQueue addOperation:blockOperation3];

有沒有人試過下面這種情況,如果好奇就試試吧!

[blockOperation1 addDependency:blockOperation1];

2. GCD

大多數(shù)開發(fā)者都知道在主線程里同步執(zhí)行任務(wù)會造成死鎖,一起來看看還有哪些情況下會造成死鎖或類似問題。

a. 在主線程同步執(zhí)行 造成 EXC_BAD_INSTRUCEION 錯誤:

- (void)deadlock1 {
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"task 1 start");
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"task 1 over");
    });
}

b. 和主線程同步執(zhí)行類似,在串行隊列中嵌套使用同步執(zhí)行任務(wù),同步隊列 task1 執(zhí)行完成后才能執(zhí)行 task2 ,而 task1 中嵌套了task2 導(dǎo)致 task1 注定無法完成。

- (void)deadlock2 {
    dispatch_queue_t queue = dispatch_queue_create("com.xietao3.sync", DISPATCH_QUEUE_SERIAL);

    dispatch_sync(queue, ^{ // 此處異步同樣會造成互相等待
        NSLog(@"task 1 start");
        dispatch_sync(queue, ^{
            NSLog(@"task 2 start");
            [NSThread sleepForTimeInterval:1.0];
            NSLog(@"task 2 over");
        });
        NSLog(@"task 1 over");
    });
}

嵌套同步執(zhí)行任務(wù)確實(shí)很容易出 bug ,但不是絕對,將同步隊列DISPATCH_QUEUE_SERIAL 換成并發(fā)隊列 DISPATCH_QUEUE_CONCURRENT 這個問題就迎刃而解。修改成并發(fā)隊列后案例中 task1 仍然要先執(zhí)行完嵌套在其中的 task2 ,而 task2 開始執(zhí)行時,并發(fā)隊列不會發(fā)生互相等待導(dǎo)致阻塞問題 , task2 執(zhí)行完成后 task1 繼續(xù)執(zhí)行。

c. 在很多人印象中,異步執(zhí)行不容易發(fā)生互相等待的情況,確實(shí),即使是串行隊列,異步任務(wù)會等待當(dāng)前任務(wù)執(zhí)行后再開始,除非你加了一些不健康的佐料。

- (void)deadlock3 {
    dispatch_queue_t queue = dispatch_queue_create("com.xietao3.asyn", DISPATCH_QUEUE_SERIAL);
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    
    dispatch_async(queue, ^{
        __block NSString *str = @"xietao3";                             // 線程1 創(chuàng)建數(shù)據(jù)
        dispatch_async(queue, ^{
            str = [NSString stringWithFormat:@"%ld",[str hash]];        // 線程2 加工數(shù)據(jù)
            dispatch_semaphore_signal(semaphore);
        });
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"%@",str);                                               // 線程1 使用加工后的數(shù)據(jù)
    });
}

d. 常規(guī)死鎖,在已經(jīng)上鎖的情況下再次上鎖,形成彼此等待的局面。

  if (!_lock) _lock = [NSLock new];
  dispatch_queue_t queue = dispatch_queue_create("com.xietao3.sync", DISPATCH_QUEUE_CONCURRENT);
    
    [_lock lock];
    dispatch_sync(queue, ^{
        [_lock lock];
        [NSThread sleepForTimeInterval:1.0];
        [_lock unlock];
    });
    [_lock unlock];

要解決也比較簡單,將NSLock換成遞歸鎖NSRecursiveLock,遞歸鎖就像普通的門鎖,順時針轉(zhuǎn)一圈加鎖后,逆時針一圈即解鎖;而如果順時針兩圈,同樣逆時針兩圈即可解鎖。下面來一個遞歸的例子:

// 以下代碼可以理解為順時針轉(zhuǎn)10圈上鎖,逆時針轉(zhuǎn)10圈解鎖
- (void)recursivelock:(int)count {
    if (count>10) return;
    count++;
    if (!_recursiveLock) _recursiveLock = [NSRecursiveLock new];

    [_recursiveLock lock];
    NSLog(@"task%d start",count);
    [self recursivelock:count];
    NSLog(@"task%d over",count);
    [_recursiveLock unlock];
}

3. 其他

除了上面提到的互斥鎖和遞歸鎖,其他的鎖還有:

  • OSSpinLock(自旋鎖)
  • pthread_mutex(OC中鎖的底層實(shí)現(xiàn))
  • NSConditionLock(條件鎖,對于新手更容易產(chǎn)生死鎖)
  • NSCondition(條件鎖的底層實(shí)現(xiàn))
  • @synchronized(對象鎖)

大部分鎖觸發(fā)死鎖的情況和互斥鎖基本一致,NSConditionLock使用起來會更加靈活,而自旋鎖雖然性能爆表,但是存在漏洞,希望了解更多關(guān)于鎖的知識可以點(diǎn)這里,在看的同時不要忘記親自動手驗(yàn)證一下,邊看邊寫邊驗(yàn)證,記得更加深刻。

總結(jié)

關(guān)于多線程、鎖的文章已經(jīng)爛大街了,本文盡可能地從新的角度來看問題,盡量不寫那些重復(fù)的內(nèi)容,希望對你有所幫助,如果文中內(nèi)容有誤,歡迎指出。

轉(zhuǎn)載請注明原文:http://www.lxweimin.com/p/0ed2858e0b51

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,106評論 6 542
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,441評論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,211評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,736評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,475評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,834評論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,829評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,009評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,559評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,306評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,516評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,038評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,728評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,132評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,443評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,249評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,484評論 2 379

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