筆記 - 多線程之GCD

目錄

  • 概念
  • 常用API的使用
  • 關於GCD的面試題

一、概念

  • 1.1、什麼是GCD?

定義想執行的任務,並追加到適當的 Dispatch Queue中

  • 1.2、GCD的隊列的概念
併發隊列(Concurrent Dispatch Queue)
- 可以讓多個任務併發(同時)執行(自動開啓多個線程同時執行任務)
- 併發功能只有在異步(dispatch_async)函數下才有效


串行隊列(Serial Dispatch Queue)
- 讓任務一個接着一個地執行(一個任務執行完畢後,再執行下一個任務)
  • 1.3、容易混淆的術語 (同步、異步、併發、串行)
同步和異步主要影響:能不能開啓新線程
- 同步:在當前線程中執行任務,不具備開啓新線程的能力
- 異步:在新的線程中執行任務,具備開啓新線程的能力


併發和串行主要影響:任務的執行方式
- 併發:多個任務併發(同時)執行
- 串行:一個任務執行完畢後,再執行下一個任務

二、常用API的使用

  • 2.1、dispatch_sync
dispatch_sync(<#dispatch_queue_t  _Nonnull queue#>,
              <#^(void)block#>)

用同步的方式執行任務
- 參數一、隊列
- 參數二、任務
  • 2.2、dispatch_async
dispatch_async(<#dispatch_queue_t  _Nonnull queue#>,
               <#^(void)block#>)

- 參數一、隊列
- 參數二、任務
  • 2.3、獲取 Dispatch Queue

  • 2.3.1、通過API的生成(dispatch_queue_create)

dispatch_queue_create(<#const char * _Nullable label#>,
                      <#dispatch_queue_attr_t  _Nullable attr#>);

參數一:指定生成返回的Dispatch Queue的名稱
- 推薦使用應用程序ID這種逆序全程域名,在Instruments及CrashLog中方便調試

參數二:指定生成返回的Dispatch Queue的類型
- 指定爲 NULL 或 DISPATCH_QUEUE_SERIAL,生成Serial Dispatch Queue;
- 指定爲DISPATCH_QUEUE_CONCURRENT,生成Concurrent Dispatch Queue
例:
dispatch_queue_t creatQueue1 = dispatch_queue_create("com.example.gcd.xxx",
                                                     NULL);
dispatch_queue_t creatQueue2 = dispatch_queue_create("com.example.gcd.xxx",
                                                     DISPATCH_QUEUE_SERIAL);
dispatch_queue_t creatQueue3 = dispatch_queue_create("com.example.gcd.xxx",
                                                     DISPATCH_QUEUE_CONCURRENT);
  • 2.3.2、獲取系統提供的Dispatch Queue
dispatch_queue_t mainQueue = dispatch_get_main_queue();
- 在主線程中執行的 Dispatch Queue


dispatch_queue_t globeQueue = dispatch_get_global_queue(<#long identifier#>,
                                                        <#unsigned long flags#>)
- 所有應用程序都能夠使用的 Concurrent Dispatch Queue
- 優先級:
#define DISPATCH_QUEUE_PRIORITY_HIGH 2                // 高優先級
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0             // 默認優先級
#define DISPATCH_QUEUE_PRIORITY_LOW (-2)              // 低優先級
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN  // 後臺優先級
- ⚠️:通過XNU內核用於Global Dispatch Queue的線程不能保證實時性能,因此執行優先級只是大致的判斷

使用:
dispatch_queue_t globeQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH,
                                                        0);
dispatch_queue_t globeQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
                                                        0);
dispatch_queue_t globeQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW,
                                                        0);
dispatch_queue_t globeQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND,
                                                        0);
  • 2.4、dispatch_after
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
                             (int64_t)(<#delayInSeconds#> * NSEC_PER_SEC)),
               dispatch_get_main_queue(), ^{
});

⚠️在多長時間之後執行,這是我們常用的API之一,現在說一下需要注意的點:
- dispatch_after函數不是在指定時間後執行處理,而是在指定時間追加處理到 Dispatch Queue;
- 本身存在延遲:執行的時間 = 傳入參數 + runloop循環1次的時間 
  • 2.5、dispatch_once
- (instancetype)init {
    if (self = [super init]) {
        static int initialized = NO;
        if (initialized == NO) {
            // 初始化單例
        }
        initialized = YES;
    }
    return self;
}

- (instancetype)init {
    if (self = [super init]) {
        static dispatch_once_t pred;
        dispatch_once(&pred, ^{
            // 初始化單例
        });
    }
    return self;
}

第一段代碼存在的隱患:
如果在多核CPU中,在正在更新表示是否初始化的標誌變量時讀取,就有可能多次執行初始化處理。
  • 2.6、Dispatch Group

使用場景:在追加到Dispatch Queue中多個處理全部結束後想執行結果處理。也就是說等多個異步線程執行完成後執行

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();

dispatch_async(queue, ^{
    NSLog(@"執行一");
});
dispatch_async(queue, ^{
    NSLog(@"執行二");
});    
dispatch_async(queue, ^{
    NSLog(@"執行三");
});

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    NSLog(@"三條線程執行完成");
});


2019-10-07 12:24:22.914339+0800 GCD[10825:325226] 執行二
2019-10-07 12:24:22.914345+0800 GCD[10825:325223] 執行一
2019-10-07 12:24:22.914369+0800 GCD[10825:325224] 執行三
2019-10-07 12:24:22.930029+0800 GCD[10825:325078] 三條線程執行完成

也可以使用 dispatch_group_wait函數僅等待全部處理執行結束

dispatch_group_wait(<#dispatch_group_t  _Nonnull group#>,
                    <#dispatch_time_t timeout#>)

- 參數一、傳入線程組
- 參數二、指定等待的時間 (DISPATCH_TIME_FOREVER意味着永久等待)



dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();

dispatch_async(queue, ^{
    NSLog(@"執行一");
});
dispatch_async(queue, ^{
    NSLog(@"執行二");
});    
dispatch_async(queue, ^{
    NSLog(@"執行三");
});
sleep(3);

dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"三條線程執行完成");
  • 2.7、dispatch_barrier_async(柵欄函數)

在進程管理中起到一個柵欄的作用,它等待所有位於barrier函數之前的操作執行完畢後執行,並且在barrier函數執行之後,barrier函數之後的操作纔會得到執行,
⚠️該函數需要同dispatch_queue_create函數生成的concurrent Dispatch Queue隊列一起使用

dispatch_queue_t queue = dispatch_queue_create("com.example.gcd", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
    NSLog(@"執行一");
});
dispatch_async(queue, ^{
    NSLog(@"執行二");
});
dispatch_barrier_async(queue, ^{
    sleep(3);
    NSLog(@"xxxxxxxxxx");
});
dispatch_async(queue, ^{
    NSLog(@"執行三");
});
dispatch_async(queue, ^{
    NSLog(@"執行四");
});


2019-10-07 12:48:55.348575+0800 GCD[11221:340677] 執行一
2019-10-07 12:48:55.348582+0800 GCD[11221:340680] 執行二
2019-10-07 12:48:58.350728+0800 GCD[11221:340680] xxxxxxxxxx
2019-10-07 12:48:58.350999+0800 GCD[11221:340678] 執行四
2019-10-07 12:48:58.351001+0800 GCD[11221:340680] 執行三
  • 2.8、dispatch_set_target_queue
    更改Dispatch Queue的執行優先級

  • 2.9、dispatch_apply
    該函數按指定的次數將指定的Block追加到指定的Dispatch Queue中,並等待全部處理執行結束

dispatch_queue_t queue = dispatch_queue_create("com.example.gcd", DISPATCH_QUEUE_CONCURRENT);
dispatch_apply(5, queue, ^(size_t index) {
    NSLog(@"------%zu", index);
});
NSLog(@"執行完成");


2019-10-07 16:18:05.663250+0800 GCD[12009:372598] ------0
2019-10-07 16:18:05.663277+0800 GCD[12009:372679] ------1
2019-10-07 16:18:05.663277+0800 GCD[12009:372683] ------2
2019-10-07 16:18:05.663302+0800 GCD[12009:372678] ------3
2019-10-07 16:18:05.663446+0800 GCD[12009:372679] ------4
2019-10-07 16:18:05.663581+0800 GCD[12009:372598] 執行完成

⚠️:由於dispatch_apply函數也與 dispatch_sync函數相同,會等待處理執行結束,因此推薦在dispatch_async函數中非同步地執行dispatch_apply函數

  • 3.0、dispatch_suspenddispatch_resume
  • 3.1、Dispatch Semaphore
    持有計數信號

三、關於GCD的面試題

  • 你理解的多線程?
  • iOS的多線程方案有哪幾種?你更傾向於哪一種?
  • 你在項目中用過GCD麼
  • GCD的隊列類型
  • 說一下OperationQueue和GCD的區別,以及各自的優勢
  • 線程安全的處理手段有哪些?
  • OC你瞭解的鎖有哪些?在你回答基礎上進行二次提問?
    自旋鎖和互斥鎖對比?使用以上鎖需要注意哪些?使用C/OC/C++,任選其一,實現自旋或互斥?口述即可
  • 3.1、iOS的多線程方案有哪幾種?你更傾向於哪一種?
  • 3.2、自旋鎖、互斥鎖比較
什麼情況使用自旋鎖比較划算?

- 預計線程等待鎖的時間很短
- 加鎖的代碼(臨界區)經常被調用,但競爭情況很少發生
- CPU資源不緊張


什麼情況使用互斥鎖比較划算?
- 預計線程等待鎖的時間較長
- 單核處理器
- 臨界區有IO操作
- 臨界區代碼複雜或者循環量大
- 臨界區競爭非常激烈
  • 3.3、關於死鎖的相關面試題
NSLog(@"執行任務1");
dispatch_queue_t main = dispatch_get_main_queue();
dispatch_sync(main, ^{
    NSLog(@"執行任務2");
});
NSLog(@"執行任務3");

2019-10-07 10:30:04.809292+0800 GCD[10060:304837] 執行任務1
❌產生死鎖
NSLog(@"執行任務1");
dispatch_queue_t main = dispatch_get_main_queue();
dispatch_async(main, ^{
    NSLog(@"執行任務2");
});
NSLog(@"執行任務3");

2019-10-07 10:29:03.893940+0800 GCD[10038:303891] 執行任務1
2019-10-07 10:29:03.894175+0800 GCD[10038:303891] 執行任務3
2019-10-07 10:29:03.918505+0800 GCD[10038:303891] 執行任務2
NSLog(@"執行任務1");
dispatch_queue_t queue = dispatch_queue_create("com.example.gcd.xxx", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
    NSLog(@"執行任務2");
    dispatch_sync(queue, ^{
        NSLog(@"執行任務3");
    });
    NSLog(@"執行任務4");
});
NSLog(@"執行任務5");


2019-10-07 10:31:09.843690+0800 GCD[10091:306352] 執行任務1
2019-10-07 10:31:09.843905+0800 GCD[10091:306352] 執行任務5
2019-10-07 10:31:09.843937+0800 GCD[10091:306413] 執行任務2
❌產生死鎖

通過上述代碼我們可以得出一個結論:使用sync函數往當前串行隊列中添加任務,會卡住當前的串行隊列
如果某些場景需要這樣做,可以使用兩個隊列來避免上面產生死鎖問題。

NSLog(@"執行任務1");
dispatch_queue_t queue = dispatch_queue_create("com.example.gcd.1", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("com.example.gcd.2", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
    NSLog(@"執行任務2");
    dispatch_sync(queue2, ^{
        NSLog(@"執行任務3");
    });
    NSLog(@"執行任務4");
});
NSLog(@"執行任務5");

後續待擴展(線程鎖)、GNUstep

這篇文章寫的非常棒,可以看看 iOS多線程--徹底學會多線程之『GCD』

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章