iOS 多執行緒(四):GCD原始碼分析下(柵欄、訊號量、排程組、dispatch_source)

語言: CN / TW / HK

準備

一、柵欄函式的應用

作用

  • 控制任務執行順序,同步。

函式

  • dispatch_barrier_async:前面的任務執行完畢才會來到這裡,這裡執行完畢才會執行後邊的任務。
  • dispatch_barrier_sync:作用相同,但是這個會堵塞執行緒,影響後面的任務執行。
  • 非常重要的一點:柵欄函式只能控制同一併發佇列。

dispatch_barrier_async 示例

``` - (void)myDemo { dispatch_queue_t concurrentQueue = dispatch_queue_create("ssl", DISPATCH_QUEUE_CONCURRENT);

// 非同步函式
dispatch_async(concurrentQueue, ^{
    sleep(1);
    NSLog(@"任務1");
});
// 非同步函式
dispatch_async(concurrentQueue, ^{
    sleep(1);
    NSLog(@"任務2");
});
// 柵欄函式
dispatch_barrier_async(concurrentQueue, ^{
    sleep(1);
    NSLog(@"------barrier------");
});
// 非同步函式
dispatch_async(concurrentQueue, ^{
    NSLog(@"任務3");
});

NSLog(@"--------乾乾幹--------");

}

執行結果: --------乾乾幹-------- 任務1 任務2 -------barrier------- 任務3 ```

  • 任務1任務2barrier中都是有延遲,但執行結果是先執行了任務1任務2,然後執行barrier,最後執行下面的任務3,確實如上面所說前面的任務執行完才會執行barrier和後面的任務。

dispatch_barrier_sync 示例

``` - (void)myDemo { dispatch_queue_t concurrentQueue = dispatch_queue_create("ssl", DISPATCH_QUEUE_CONCURRENT);

// 非同步函式
dispatch_async(concurrentQueue, ^{
    sleep(1);
    NSLog(@"任務1");
});
// 非同步函式
dispatch_async(concurrentQueue, ^{
    sleep(1);
    NSLog(@"任務2");
});
// 柵欄函式
dispatch_barrier_sync(concurrentQueue, ^{
    sleep(1);
    NSLog(@"------barrier------");
});
// 非同步函式
dispatch_async(concurrentQueue, ^{
    NSLog(@"任務3");
});

NSLog(@"--------乾乾幹--------");

}

執行結果: 任務2 任務1 ------barrier------ --------乾乾幹-------- 任務3 ```

  • 可以看到sync函式不僅有async函式的作用,還有堵塞執行緒的作用,主執行緒中的乾乾幹也是在barrier後面才執行。

dispatch_get_global_queue 示例

將普通佇列換成全域性併發佇列,檢視執行結果:

``` - (void)myDemo { dispatch_queue_t concurrentQueue = dispatch_get_global_queue(0, 0);

// 非同步函式
dispatch_async(concurrentQueue, ^{
    sleep(1);
    NSLog(@"任務1");
});
// 非同步函式
dispatch_async(concurrentQueue, ^{
    sleep(1);
    NSLog(@"任務2");
});
// 柵欄函式
dispatch_barrier_async(concurrentQueue, ^{
    sleep(1);
    NSLog(@"------barrier------");
});
// 非同步函式
dispatch_async(concurrentQueue, ^{
    NSLog(@"任務3");
});

NSLog(@"--------乾乾幹--------");

}

執行結果: --------乾乾幹-------- 任務3 ------barrier------ 任務2 任務1 ```

  • 可以看到在全域性併發佇列中,柵欄函式的作用失效了。

流程控制案例

看下面這個案例:

``` - (void)myDemo { dispatch_queue_t concurrentQueue = dispatch_queue_create("ssl", DISPATCH_QUEUE_CONCURRENT); // 多執行緒 操作marray for (int i = 0; i < 1000; i++) { dispatch_async(concurrentQueue, ^{ NSString imageName = [NSString stringWithFormat:@"%d.jpg", (i % 10)]; NSURL url = [[NSBundle mainBundle] URLForResource:imageName withExtension:nil]; NSData data = [NSData dataWithContentsOfURL:url]; UIImage image = [UIImage imageWithData:data];

        [self.mArray addObject:image];
    });
}

}

  • (void)touchesBegan:(NSSet )touches withEvent:(UIEvent )event{ NSLog(@"陣列的個數:%zd",self.mArray.count); }

執行結果: 陣列的個數:996 ```

  • 陣列的個數不一定是固定的996,但是是小於1000的。
  • 因為這樣操作執行緒是不安全的,當兩個任務同時進行addObject操作時,假設這時mArray的數量是800,那麼兩個任務都會向801的位置進行賦值操作,其中的一個就會被覆蓋,從而導致最終陣列的個數小於1000

[self.mArray addObject:image]放到柵欄函式中,來保證執行緒的安全:

``` - (void)myDemo { ... dispatch_barrier_async(concurrentQueue , ^{ [self.mArray addObject:image]; }); ... }

  • (void)touchesBegan:(NSSet )touches withEvent:(UIEvent )event{ NSLog(@"陣列的個數:%zd",self.mArray.count); }

執行結果: 陣列的個數:1000 `` * 柵欄函式會保證前面的任務執行完畢,再執行後面的任務,任務會一個一個的執行,保證了執行緒的安全,所以最終陣列的個數是1000`。

二、柵欄函式的底層原理

我們以同步的柵欄函式進行底層原理的分析。

柵欄函式之前的任務執行

進入dispatch_barrier_async

image.png

進入_dispatch_barrier_sync_f -> _dispatch_barrier_sync_f_inline -> _dispatch_sync_f_slow

image.png

符號斷點跟流程:

image.png

  • 可以看到柵欄之前的任務是在__DISPATCH_WAIT_FOR_QUEUE__中執行的,這個函式在 上一篇 也提到過,死鎖就是在這個函式中發生的。

柵欄函式之後的任務執行

接下來進入_dispatch_sync_invoke_and_complete_recurse -> _dispatch_sync_complete_recurse

static void _dispatch_sync_complete_recurse(dispatch_queue_t dq, dispatch_queue_t stop_dq, uintptr_t dc_flags) { bool barrier = (dc_flags & DC_FLAG_BARRIER); // 迴圈操作 do { if (dq == stop_dq) return; // 是否存在柵欄 if (barrier) { // 執行柵欄函式 // #define dx_wakeup(x, y, z) dx_vtable(x)->dq_wakeup(x, y, z) dx_wakeup(dq, 0, DISPATCH_WAKEUP_BARRIER_COMPLETE); } else { // 再次進來時,沒有柵欄函式,一些狀態的修改 _dispatch_lane_non_barrier_complete(upcast(dq)._dl, 0); } dq = dq->do_targetq; barrier = (dq->dq_width == 1); } while (unlikely(dq->do_targetq)); }

斷點跟流程驗證:

image.png

接下來會繼續呼叫到_dispatch_sync_f_slow函式,完成所有任務的執行:

image.png

總結

  • 首先通過__DISPATCH_WAIT_FOR_QUEUE__函式將先加入佇列的任務執行完。
  • 然後do while迴圈,判斷如果有barrier,先執行barrier任務,最後再迴圈執行剩下的任務。

三、訊號量的使用

作用

  • 同步->當鎖,控制GCD最大併發數。

相關函式

  • dispatch_semaphore_create:建立訊號量。
  • dispatch_semaphore_wait:訊號量等待。
  • dispatch_semaphore_signal:訊號量釋放。

示例

``` - (void)myDemo { dispatch_queue_t queue = dispatch_get_global_queue(0, 0); dispatch_semaphore_t sem = dispatch_semaphore_create(1);

//任務1
dispatch_async(queue, ^{
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); // 等待
    sleep(2);
    NSLog(@"執行任務1");
    NSLog(@"任務1完成");
    dispatch_semaphore_signal(sem); // 發訊號
});
//任務2
dispatch_async(queue, ^{
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); // 等待
    sleep(1);
    NSLog(@"執行任務2");
    NSLog(@"任務2完成");
    dispatch_semaphore_signal(sem); // 發訊號
});
//任務3
dispatch_async(queue, ^{
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); // 等待
    NSLog(@"執行任務3");
    NSLog(@"任務3完成");
    dispatch_semaphore_signal(sem);
});
NSLog(@"任務4");

}

執行結果:

任務4 執行任務1 任務1完成 執行任務2 任務2完成 執行任務3 任務3完成 ```

  • 任務1任務2都有延遲操作,它們本應該後執行的,因為加了訊號量,任務卻是從上到下執行的,任務1任務2反而先執行了。

四、訊號量的底層原理

dispatch_semaphore_create

image.png

  • 可以看到訊號量的建立主要就是給dsema_value賦值,賦值為我們傳進來的值,接下來繼續分析。

dispatch_semaphore_wait

intptr_t dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout) { long value = os_atomic_dec2o(dsema, dsema_value, acquire); if (likely(value >= 0)) { return 0; } return _dispatch_semaphore_wait_slow(dsema, timeout); }

  • dsema_value進行--操作,如果>= 0,函式返回0,任務可以執行。
  • 否則,返回_dispatch_semaphore_signal_slow函式。

進入_dispatch_semaphore_wait_slow

image.png

  • timeoutDISPATCH_TIME_FOREVER,將會進行等待。

進入_dispatch_sema4_wait函式:

image.png

  • 可以看到這裡是一個do while迴圈,所以dispatch_semaphore_wait下面的程式碼不能執行。
  • dsema_value >= 0do while迴圈結束,任務又可以正常執行,這就涉及到了dsema_value++操作,下面來看dispatch_semaphore_signal函式。

dispatch_semaphore_signal

dispatch_semaphore_signal(dispatch_semaphore_t dsema) { long value = os_atomic_inc2o(dsema, dsema_value, release); if (likely(value > 0)) { return 0; } if (unlikely(value == LONG_MIN)) { DISPATCH_CLIENT_CRASH(value, "Unbalanced call to dispatch_semaphore_signal()"); } return _dispatch_semaphore_signal_slow(dsema); }

  • dsema_value進行++操作,如果> 0,函式返回0
  • 如果value == LONG_MIN,程式崩潰。
  • 上面的條件都不成立,返回_dispatch_semaphore_signal_slow函式。

進入_dispatch_semaphore_signal_slow函式,可以看到是一些異常的處理:

image.png

總結

  • 訊號量操作主要就是dsema_value的相關處理。
  • dispatch_semaphore_create初始化dsema_value值。
  • dispatch_semaphore_wait進行dsema_value--操作,如果>= 0任務執行,否則進行do while迴圈阻塞任務執行,等待dsema_value>= 0任務可以繼續執行。
  • dispatch_semaphore_signal進行dsema_value++操作。

五、排程組的應用

作用

  • 控制任務執行順序。

函式

  • dispatch_group_create:建立組。
  • dispatch_group_async:進組任務。
  • dispatch_group_notify:進組任務執行完畢通知。
  • dispatch_group_wait:進組任務執行等待時間。
  • dispatch_group_enter:進組。 
  • dispatch_group_leave:出組。

示例

``` - (void)myGroupDemo { dispatch_group_t group = dispatch_group_create(); dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

//建立排程組
dispatch_group_async(group, queue, ^{
    sleep(1);
    [self.mArray addObject:@"圖片1"];
});

// 進組和出租 成對 先進後出
dispatch_group_enter(group);
dispatch_async(queue, ^{
    sleep(1);
    [self.mArray addObject:@"圖片2"];
    dispatch_group_leave(group);
});

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    NSMutableString *muStr = [NSMutableString new];
    for (NSString *str in self.mArray) {
        [muStr appendString:str];
    }
    [muStr appendString:@"-生成水印"];

    NSLog(@"%@",muStr);
});

}

執行結果: 圖片1圖片2-生成水印 ```

  • 當兩個非同步任務都完成以後,才會呼叫到group任務,下面來分析一下底層原理。

六、排程組的原理

排程組早先版本是用訊號量來實現的,現在自己寫了一套,也是仿照訊號量的原理,都是關於value的一些操作。

dispatch_group_create

dispatch_group_t dispatch_group_create(void) { return _dispatch_group_create_with_count(0); }

進入_dispatch_group_create_with_count

static inline dispatch_group_t _dispatch_group_create_with_count(uint32_t n) { dispatch_group_t dg = _dispatch_object_alloc(DISPATCH_VTABLE(group), sizeof(struct dispatch_group_s)); dg->do_next = DISPATCH_OBJECT_LISTLESS; dg->do_targetq = _dispatch_get_default_queue(false); if (n) { os_atomic_store2o(dg, dg_bits, (uint32_t)-n * DISPATCH_GROUP_VALUE_INTERVAL, relaxed); os_atomic_store2o(dg, do_ref_cnt, 1, relaxed); // <rdar://22318411> } return dg; }

  • 這裡是進行了一些初始化的操作。

dispatch_group_enter

void dispatch_group_enter(dispatch_group_t dg) { // The value is decremented on a 32bits wide atomic so that the carry // for the 0 -> -1 transition is not propagated to the upper 32bits. uint32_t old_bits = os_atomic_sub_orig2o(dg, dg_bits, DISPATCH_GROUP_VALUE_INTERVAL, acquire); uint32_t old_value = old_bits & DISPATCH_GROUP_VALUE_MASK; if (unlikely(old_value == 0)) { _dispatch_retain(dg); // <rdar://problem/22318411> } if (unlikely(old_value == DISPATCH_GROUP_VALUE_MAX)) { DISPATCH_CLIENT_CRASH(old_bits, "Too many nested calls to dispatch_group_enter()"); } }

  • value進行--操作。
  • 如果old_value等於0,呼叫_dispatch_retain函式
  • 如果old_value等於DISPATCH_GROUP_VALUE_MAX,程式崩潰,DISPATCH_GROUP_VALUE_MAX的值為4#define DISPATCH_GROUP_VALUE_INTERVAL   0x0000000000000004ULL #define DISPATCH_GROUP_VALUE_MAX        DISPATCH_GROUP_VALUE_INTERVAL

dispatch_group_leave

image.png

  • 這裡會執行value++操作,如果old_value等於-1,呼叫_dispatch_group_wake執行喚醒操作,喚醒操作會呼叫dispatch_group_notify中的block任務。

dispatch_group_notify

image.png

  • old_state == 0也就是value == 0時,執行_dispatch_group_wake,呼叫dispatch_group_notify中的block任務。

dispatch_group_async

我們猜測dispatch_group_async函式中應該是有這dispatch_group_enterdispatch_group_leave的呼叫,接下來進行探索。

進入dispatch_group_async

image.png

進入_dispatch_continuation_group_async

image.png

  • 在這裡找到了dispatch_group_enter函式的呼叫,繼續看_dispatch_continuation_async

我們在 iOS 多執行緒(二):GCD基礎&原始碼分析上 中分析過,通過_dispatch_continuation_async最終會呼叫到_dispatch_continuation_invoke_inline函式:

image.png

  • 我們之前分析的正常情況會呼叫_dispatch_client_callout函式,但如果標記是DC_FLAG_GROUP_ASYNC的時候,呼叫的是_dispatch_continuation_with_group_invoke函式。

進入_dispatch_continuation_with_group_invoke

image.png

  • 如上圖,當type == DISPATCH_GROUP_TYPE時,先通過_dispatch_client_callout完成block任務的呼叫,然後就會呼叫dispatch_group_leave函式,完美!!。

總結

  • dispatch_group_create:初始化value = 0;
  • dispatch_group_entervalue--;
  • dispatch_group_leavevalue++,如果old_value == -1,呼叫dispatch_group_notify中的block任務。
  • dispatch_group_notify:如果value == 0,呼叫dispatch_group_notify中的block任務。
  • dispatch_group_async:內部會呼叫成對的dispatch_group_enterdispatch_group_leave

七、dispatch_source

作用

  • CPU負荷非常小,儘量不佔用資源。
  • 聯結的優勢。

概念

在任一執行緒上呼叫它的一個函式dispatch_source_merge_data後,會執行Dispatch Source實現定義好的控制代碼(可以把控制代碼簡單理解為一個block)這個過程叫Custom event,使用者事件,是dispatch source支援處理的一種事件。

控制代碼是一種指向指標的指標,它指向的就是一個類或者結構,它和系統有很密切的關係

HINSTANCE(例項控制代碼),HBITMAP(點陣圖控制代碼),HDC(裝置表述控制代碼),HICON(圖示控制代碼)等。這當中還有一個通用的控制代碼,就是HANDLE

函式

  • dispatch_source_create:建立源。
  • dispatch_source_set_event_handler:設定源事件回撥。
  • dispatch_source_merge_data:源事件設定資料。
  • dispatch_source_get_data:獲取源事件資料。 
  • dispatch_resume:繼續。 
  • dispatch_suspend:掛起。

基本使用

```

import "ViewController.h"

@interface ViewController () @property (weak, nonatomic) IBOutlet UIProgressView *progressView; @property (nonatomic, strong) dispatch_source_t source; @property (nonatomic, strong) dispatch_queue_t queue;

@property (nonatomic, assign) NSUInteger totalComplete; @property (nonatomic) BOOL isRunning; @end

@implementation ViewController

  • (void)viewDidLoad { [super viewDidLoad];

    self.totalComplete = 0;

    self.queue = dispatch_queue_create("com.ssl.dd", 0);

    第一個引數:dispatch_source_type_t type為設定GCD源方法的型別,前面已經列舉過了。 第二個引數:uintptr_t handle Apple的API介紹說,暫時沒有使用,傳0即可。 第三個引數:unsigned long mask Apple的API介紹說,使用DISPATCH_TIMER_STRICT,會引起電量消耗加劇,畢竟要求精確時間,所以一般傳0即可,視業務情況而定。 第四個引數:dispatch_queue_t _Nullable queue 佇列,將定時器事件處理的Block提交到哪個佇列之上。可以傳Null,預設為全域性佇列。注意:當提交到全域性佇列的時候,時間處理的回撥內,需要非同步獲取UI執行緒,更新UI...不過這好像是常識,又囉嗦了... */ self.source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());

    // 儲存程式碼塊 ---> 非同步 dispatch_source_set_event_handler() // 設定取消回撥 dispatch_source_set_cancel_handler(dispatch_source_t source,dispatch_block_t _Nullable handler) // 封裝我們需要回調的觸發函式 -- 響應 dispatch_source_set_event_handler(self.source, ^{

    NSUInteger value = dispatch_source_get_data(self.source); // 取回來值 1 響應式
    self.totalComplete += value;
    NSLog(@"進度:%.2f", self.totalComplete/100.0);
    self.progressView.progress = self.totalComplete/100.0;
    

    });

    self.isRunning = YES; dispatch_resume(self.source); }

  • (IBAction)didClickStartOrPauseAction:(id)sender {

    if (self.isRunning) {// 正在跑就暫停 dispatch_suspend(self.source); dispatch_suspend(self.queue);// mainqueue 掛起 self.isRunning = NO; [sender setTitle:@"暫停中..." forState:UIControlStateNormal]; }else{ dispatch_resume(self.source); dispatch_resume(self.queue); self.isRunning = YES; [sender setTitle:@"載入中..." forState:UIControlStateNormal]; } }

  • (void)touchesBegan:(NSSet )touches withEvent:(UIEvent )event{

    NSLog(@"點選開始載入"); for (NSUInteger index = 0; index < 100; index++) { dispatch_async(self.queue, ^{ if (!self.isRunning) { NSLog(@"暫停下載"); return ; } sleep(2);

        dispatch_source_merge_data(self.source, 1); // source 值響應
    });
    

    } } ```

列印結果:

image.png