你不知道的的 iOS 多線程

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

有言在先

首先我不是故意要作標題黨的,也不是我要炒冷飯,我只是想換個姿式看多線程,本文大部份內容在分析如何造死鎖,奈何功力尚淺,然而再淺,也須要走出第一步。打開你的 Xcode 來驗證這些死鎖吧。安全

多線程小知識

如下是實現多線程的三種方式:bash

  • NSThread
  • GCD
  • NSOperationQueue

關於具體使用的方法再也不具體介紹,讓咱們來看看他們鮮爲人知的一面多線程

1. 鎖的背後

NSLock是基於 POSIX threads 實現的,而 POSIX threads 中使用互斥量同步線程。併發

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

  • 原子性 - 鎖住一個互斥量是一個原子操做,代表操做系統保證若是在你已經鎖了一個互斥量,那麼在同一時刻就不會有其餘線程可以鎖住這個互斥量;異步

  • 奇異性 - 若是一個線程鎖住了一個互斥量,那麼能夠保證的是在該線程釋放這個鎖以前沒有其餘線程能夠鎖住這個互斥量;async

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

2. 關於生命週期

經過 [NSThread exit] 方法使線程退出 ,NSThread 是能夠當即終止正在執行的任務(可能會形成內存泄露,這裏不深究)。甚至你能夠在主線程中執行該操做,會使主線程也退出,app 沒法再響應事件。而 cancel 能夠經過做爲標誌位來達到相似目的,若是不作任何處理,仍然會繼續執行。性能

GCD和NSOperationQueue能夠取消隊列中未開始執行的任務,對於已經開始執行的任務就無能爲力了。

實現方式\功能 線程生命週期 取消任務
NSThread 手動管理 當即中止執行
GCD 自動管理 取消隊列中未執行的任務
NSOperationQueue 自動管理 取消隊列中未執行的任務

3. 並行與併發

看到不少文章裏提到 併發隊列 ,這裏有一個小陷阱,混淆了 併發並行 的概念。咱們先來看看一下他們之間的區別:

併發與並行

從圖中能夠看到,並行纔是真正的多線程,而併發只是在多任務中切換。通常多核CPU能夠並行執行多個線程,而單核CPU實際上只有一個線程,多路複用達到接近同時執行的效果。在 iOS 中 dispatch_async 和 globalQueue 從 Xcode 中線程使用狀況來看,都達到了並行的效果。

4. 隊列與線程

隊列是保存以及管理任務的,將任務加到隊列中,任務會按照加入到隊列中前後順序依次執行。若是是全局隊列和併發隊列,則系統會根據系統資源去建立新的線程去處理隊列中的任務,線程的建立、維護和銷燬由操做系統管理,還有隊列自己是線程安全的。

使用 NSOperationQueue 實現多線程的時候是能夠控制線程總數及線程依賴關係的,而 GCD 只能選擇併發或者串行隊列。

資源競爭

多線程同時執行任務能提升程序的執行效率和響應時間,可是多線程不可避免地遇到同時操做同一資源的狀況。前段時間看到的一個資源競爭的問題爲例:

@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
  • 將併發隊列 DISPATCH_QUEUE_CONCURRENT 改爲串行隊列 DISPATCH_QUEUE_SERIAL
  • 異步執行dispatch_async 改爲同步執行dispatch_sync
  • 賦值使用@synchronized 或者上鎖。

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

花樣死鎖

任何事情都有兩面性,就像多線程能提高效率的同時,也會形成資源競爭的問題。而鎖在保證多線程的數據安全的同時,粗枝大葉之下也容易發生問題,那就是 死鎖

1. NSOperationQueue

鑑於 NSOperationQueue 高度封裝,使用起來很是簡單,通常不會出什麼幺蛾子,下面的案例展現了一個很差示範,一般咱們經過控制 NSOperation 之間的從屬關係,來達到有序執行任務的效果,可是若是互相從屬或者循環從屬都會形成全部任務沒法開始。

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");
    }];
    
    // 循環從屬
    [blockOperation2 addDependency:blockOperation1];
    [blockOperation3 addDependency:blockOperation2];
    [blockOperation1 addDependency:blockOperation3]; // 循環的罪魁禍首

    // 互相從屬
    //[blockOperation1 addDependency:blockOperation2];
    //[blockOperation2 addDependency:blockOperation1];

    [_operationQueue addOperation:blockOperation1];
    [_operationQueue addOperation:blockOperation2];
    [_operationQueue addOperation:blockOperation3];
複製代碼

有沒有人試過下面這種狀況,若是好奇就試試吧!

[blockOperation1 addDependency:blockOperation1];
複製代碼

2. GCD

大多數開發者都知道在主線程裏同步執行任務會形成死鎖,一塊兒來看看還有哪些狀況下會形成死鎖或相似問題。

a. 在主線程同步執行 形成 EXC_BAD_INSTRUCEION 錯誤:

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

b. 和主線程同步執行相似,在串行隊列中嵌套使用同步執行任務,同步隊列 task1 執行完成後才能執行 task2 ,而 task1 中嵌套了task2 致使 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");
    });
}
複製代碼

嵌套同步執行任務確實很容易出 bug ,但不是絕對,將同步隊列DISPATCH_QUEUE_SERIAL 換成併發隊列 DISPATCH_QUEUE_CONCURRENT 這個問題就迎刃而解。修改爲併發隊列後案例中 task1 仍然要先執行完嵌套在其中的 task2 ,而 task2 開始執行時,併發隊列不會發生互相等待致使阻塞問題 , task2 執行完成後 task1 繼續執行。

c. 在不少人印象中,異步執行不容易發生互相等待的狀況,確實,即便是串行隊列,異步任務會等待當前任務執行後再開始,除非你加了一些不健康的佐料。

- (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 建立數據
        dispatch_async(queue, ^{
            str = [NSString stringWithFormat:@"%ld",[str hash]];        // 線程2 加工數據
            dispatch_semaphore_signal(semaphore);
        });
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"%@",str);                                               // 線程1 使用加工後的數據
    });
}
複製代碼

d. 常規死鎖,在已經上鎖的狀況下再次上鎖,造成彼此等待的局面。

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,遞歸鎖就像普通的門鎖,順時針轉一圈加鎖後,逆時針一圈即解鎖;而若是順時針兩圈,一樣逆時針兩圈便可解鎖。下面來一個遞歸的例子:

// 如下代碼能夠理解爲順時針轉10圈上鎖,逆時針轉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中鎖的底層實現)
  • NSConditionLock(條件鎖,對於新手更容易產生死鎖)
  • NSCondition(條件鎖的底層實現)
  • @synchronized(對象鎖)

大部分鎖觸發死鎖的狀況和互斥鎖基本一致,NSConditionLock使用起來會更加靈活,而自旋鎖雖然性能爆表,可是存在漏洞,但願瞭解更多關於鎖的知識能夠點這裏,在看的同時不要忘記親自動手驗證一下,邊看邊寫邊驗證,記得更加深入。

總結

關於多線程、鎖的文章已經爛大街了,本文儘量地重新的角度來看問題,儘可能不寫那些重複的內容,但願對你有所幫助,若是文中內容有誤,歡迎指出。

轉載請註明原文:juejin.im/post/59c13d…

相關文章
相關標籤/搜索