iOS 多線程原理 - GCD函數底層

libdispatch-1271.120.2 下載
蘋果官方資源opensource

多線程相關文獻:
iOS 多線程原理 - 線程與隊列底層
iOS 多線程原理 - GCD函數底層
iOS 線程底層 - 鎖

本章節(jié)探究:
1.單例 dispatch_once
2.柵欄函數 barrier
3.調度組 group
4.信號量 semaphore
5.dispatch_source

前言

在了解了線程與隊列的底層原理之后,本章節(jié)來看看GCD函數的底層原理,研究這些API是怎么調用的,并附上使用案例。

一、單例

+ (SingleExample *)shareInstance {
    static SingleExample *single = nil;
    static dispatch_once_t onceToken ;
    dispatch_once(&onceToken, ^{
        single = [[SingleExample alloc] init];
    }) ;
    return single;
}

來看看dispatch_once這個函數原理。
打開libdispatch源碼

dispatch_once的源碼聲明:

dispatch_once

dispatch_once_t *val它里面有一個狀態(tài)的記錄,來保證block只被調用一次。

dispatch_once_f的源碼聲明:

dispatch_once_f

需要看看執(zhí)行func的要出于什么條件下才會被執(zhí)行 (_dispatch_once_gate_tryenter

_dispatch_once_gate_tryenter的源碼聲明:

_dispatch_once_gate_tryenter

而等待是怎么等的呢?(_dispatch_once_wait
_dispatch_once_wait的源碼聲明:

在死循環(huán)里不斷地查詢單例狀態(tài),一旦任務執(zhí)行完畢才跳出循環(huán)。

單例總結:
1.線程安全
2.任務只會被執(zhí)行一次
3.通過一個狀態(tài)值來保證任務是否被執(zhí)行過

二、柵欄函數

同步柵欄函數dispatch_barrier_sync案例:

- (void)test_barrier {
    dispatch_queue_t t = dispatch_queue_create("AnAn", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(t, ^{
        NSLog(@"1");
    });
    dispatch_async(t, ^{
        NSLog(@"2");
    });
    // 柵欄函數
    dispatch_barrier_sync(t, ^{
        sleep(2);
        NSLog(@"%@", [NSThread currentThread]); // main
        NSLog(@"3");
    });
    
    NSLog(@"4");
    
    dispatch_async(t, ^{
        NSLog(@"5");
    });
}
// 12順序不一定;3一定在12后面;45在3后面;45順序不一定
// 同步柵欄dispatch_barrier_sync 和 普通的同步dispatch_sync效果是一樣的

異步柵欄函數dispatch_barrier_async案例:

- (void)test_barrier {
    dispatch_queue_t t = dispatch_queue_create("AnAn", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(t, ^{
        NSLog(@"1");
    });
    dispatch_async(t, ^{
        NSLog(@"2");
    });
    // 異步柵欄函數
    dispatch_barrier_async(t, ^{
        sleep(2);
        NSLog(@"3");
    });
    
    NSLog(@"4");
    
    dispatch_async(t, ^{
        NSLog(@"5");
    });
}
// 124順序不一定,3一定在12后面,5一定在3后面
// 異步柵欄函數只能柵得住非全局隊列的任務

全局隊列案例

- (void)test_barrier_global {
    dispatch_queue_t t = dispatch_get_global_queue(0, 0);
    dispatch_async(t, ^{
        NSLog(@"1");
    });
    dispatch_async(t, ^{
        NSLog(@"2");
    });
    // 柵欄函數
    dispatch_barrier_async(t, ^{
        sleep(2);
        NSLog(@"3");
    });
    
    NSLog(@"4");
    
    dispatch_async(t, ^{
        NSLog(@"5");
    });
}

// 1245沒有順序 3在最后
// 異步柵欄函數柵不住全局隊列里的任務

柵欄函數分為同步柵欄異步柵欄。
dispatch_barrier_async在自定義的并發(fā)隊列里,全局和串行達不到我們要的效果。
蘋果文檔中指出,如果使用的是全局隊列或者創(chuàng)建的不是并發(fā)隊列,則dispatch_barrier_async實際上就相當于dispatch_async。

1.同步柵欄dispatch_barrier_sync

其實同步柵欄與普通同步實現的效果是差不多的,在源碼上只有一點點小差異。

dispatch_barrier_sync
_dispatch_barrier_sync_f
_dispatch_barrier_sync_f_inline

_dispatch_barrier_sync_f_inline里會判斷不同的隊列條件來去選擇分支繼續(xù)往下走,這里我以并發(fā)隊列為例,它會走_dispatch_sync_f_slow代碼分支:

_dispatch_sync_f_slow

以并發(fā)隊列為例,它會走_dispatch_sync_invoke_and_complete_recurse代碼分支:

_dispatch_sync_invoke_and_complete_recurse

_dispatch_sync_function_invoke_inline和dispatch_sync底層一樣的,是去調用func。
柵欄函數完成任務后會執(zhí)行_dispatch_sync_complete_recurse喚醒隊列里后續(xù)的任務。

_dispatch_sync_complete_recurse

_dispatch_sync_complete_recurse里通過do...while去喚醒隊列里的任務dx_wakeup;
dx_wakeup是一個dq_wakeup的宏定義:

#define dx_wakeup(x, y, z) dx_vtable(x)->dq_wakeup(x, y, z)
dq_wakeup

(柵欄函數柵不住全局隊列的原因就在這里,因為它指定的wakeup函數不一樣。)

喚醒以并發(fā)隊列為例,它會走_dispatch_lane_wakeup

_dispatch_lane_wakeup

為barrier形式,調用_dispatch_lane_barrier_complete

_dispatch_lane_barrier_complete
  • 如果是串行隊列,則會進行等待,等待其他的任務執(zhí)行完成,再按順序執(zhí)行;
  • 如果是并發(fā)隊列,則會調用_dispatch_lane_drain_non_barriers方法將柵欄之前的任務執(zhí)行完成;
  • 最后會調用_dispatch_lane_class_barrier_complete方法,也就是把柵欄拔掉了,不攔了,從而執(zhí)行柵欄之后的任務。

喚醒以全局并發(fā)隊列為例,它會走_dispatch_root_queue_wakeup
它里面就沒有攔截有關柵欄函數相關的東西。

_dispatch_root_queue_wakeup

同步柵欄函數dispatch_barrier_sync和普通同步函數dispatch_sync效果是一樣的:
阻塞當前線程,不開辟線程,立即執(zhí)行,同步柵欄函數還需要等柵欄任務完成后喚醒非全局隊列后續(xù)的任務

為什么蘋果設計柵欄函數柵不住全局并發(fā)隊列?
因為我們系統(tǒng)也會使用全局并發(fā)隊列,避免造成系統(tǒng)任務被阻塞。

2.異步柵欄dispatch_barrier_async
dispatch_barrier_async
  • _dispatch_continuation_init保存任務
_dispatch_continuation_init

_dispatch_continuation_init保存了任務,在需要執(zhí)行的時候拿出來執(zhí)行

_dispatch_continuation_init
  • _dispatch_continuation_async
_dispatch_continuation_async

dx_push是宏定義:

#define dx_push(x, y, z) dx_vtable(x)->dq_push(x, y, z)

找到dq_push的聲明:

dq_push

根據不同的隊列賦值給dq_push不一樣的函數
以并發(fā)隊列為例:

_dispatch_lane_concurrent_push的源碼聲明:

_dispatch_lane_concurrent_push就是柵欄異步與普通異步函數的分支:

_dispatch_lane_concurrent_push
_dispatch_lane_push

走到dx_wakeup函數,這里在同步柵欄部分已經介紹過了。

柵欄函數總結:
1.柵欄函數只針對非全局隊列;
2.柵欄函數不能柵住全局隊列,因為系統(tǒng)也在用它,防止阻塞住系統(tǒng)任務;
3.柵欄函數需要等待當前隊列前面的任務執(zhí)行完,再去執(zhí)行柵欄任務,最后喚醒執(zhí)行柵欄任務后面的任務

三、調度組

- (void)test_group {
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t que1 = dispatch_queue_create("An", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t que2 = dispatch_queue_create("Lin", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_group_enter(group);
    dispatch_async(que1, ^{
        sleep(4);
        NSLog(@"1");
        dispatch_group_leave(group);
    });

    dispatch_group_enter(group);
    dispatch_async(que2, ^{
        sleep(3);
        NSLog(@"2");
        dispatch_group_leave(group);
    });
    
    dispatch_group_enter(group);
    dispatch_async(dispatch_get_global_queue(0, 0 ), ^{
        sleep(2);
        NSLog(@"3");
        dispatch_group_leave(group);
    });
    
    dispatch_group_enter(group);
    dispatch_async(dispatch_get_main_queue(), ^{
        sleep(1);
        NSLog(@"4");
        dispatch_group_leave(group);
    });

    dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
        NSLog(@"6");
    });

    dispatch_group_notify(group, dispatch_get_global_queue(0, 0), ^{
        NSLog(@"5");
    });
}
// 5在4321任務之后

當然也可以使用dispatch_group_async來代替dispatch_group_enterdispatch_group_leave;效果是一樣的。

研究的對象有三個:dispatch_group_enter、dispatch_group_leave、dispatch_group_notify。

1. dispatch_group_enter的源碼分析
dispatch_group_enter

ps: 這里 DISPATCH_GROUP_VALUE_INTERVAL = 0x0000000000000004ULL
注釋里說的 0->-1 躍遷的進位其實是位運算。實際上是+4

蘋果官方文檔dispatch_group_enter的解釋:
調用此函數將增加組中當前未完成任務的計數。如果應用程序通過dispatch_group_async函數以外的方式顯式地從組中添加和刪除任務,那么使用這個函數(與dispatch_group_leave一起使用)允許您的應用程序正確地管理任務引用計數。對這個函數的調用必須與對dispatch_group_leave的調用相平衡。您可以使用此函數同時將一個塊與多個組關聯。

2. dispatch_group_leave的源碼分析
dispatch_group_leave

蘋果官方文檔dispatch_group_leave的解釋:
調用此函數將減少組中當前未完成任務的計數。如果應用程序通過dispatch_group_async函數以外的方式顯式地從組中添加和刪除任務,那么使用這個函數(與dispatch_group_enter一起使用)允許您的應用程序正確地管理任務引用計數。
對該函數的調用必須平衡對dispatch_group_enter的調用。調用它的次數超過dispatch_group_enter是無效的,這會導致負的計數。

3.dispatch_group_notify的源碼分析
dispatch_group_notify
dispatch_group_notify

dispatch_group_enter通過改變調度組狀態(tài)值+4;dispatch_group_leave通過調度組狀態(tài)值-4;dispatch_group_notify在調度組內不斷地獲取調度組狀態(tài)值,如果狀態(tài)值達到平衡(等于0),則說明前面的任務做完了,需要執(zhí)行notify里的任務。

調度組總結:
1.dispatch_group_enterdispatch_group_leave必須成對使用;
2.dispatch_group_leave次數多于dispatch_group_enter會導致崩潰;
3.調度組底層是通過修改調度組的狀態(tài)值的增(enter)減(leave),不斷地監(jiān)聽這個狀態(tài)值是否達到平衡(等于0),一旦平衡則去執(zhí)行dispatch_group_notify里的任務。

四、信號量

- (void)test_semaphore {
    // 設置0,任務1不需要等;設置1,任務1和2不需要等...
    dispatch_semaphore_t sem = dispatch_semaphore_create(0);
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"1");
        dispatch_semaphore_signal(sem); 
    });
    
    // 等待任務1的signal
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(2);
        NSLog(@"2");
        dispatch_semaphore_signal(sem);
    });
    
     // 等待任務2的signal
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"3");
        dispatch_semaphore_signal(sem);
    });
    
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"4");
        dispatch_semaphore_signal(sem);
    });
    
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"5");
        dispatch_semaphore_signal(sem);
    });
}
// 12345
  • dispatch_semaphore_create 創(chuàng)建信號量,指定信號量數值
  • dispatch_semaphore_signal 發(fā)送信號量,將信號量數值+1
  • dispatch_semaphore_wait 等待信號量;當信號量數值為0時,阻塞當前線程一直等待;當信號量數值大于等于1時,將信號量數值-1并執(zhí)行當前線程的任務
1.dispatch_semaphore_create的源碼聲明:
dispatch_semaphore_signal聲明
dispatch_semaphore_signal實現
  • 當兩個線程需要協(xié)調特定事件的完成時,為該值傳遞0很有用;
  • 傳遞大于0的值對于管理有限的資源池很有用,其中池大小等于該值;
  • 信號量的起始值傳遞小于信號量的起始值。 傳遞小于零的值將導致返回 NULL,也就是小于0就不會正常執(zhí)行。

總的來說:信號量初始值可以控制線程池中的最多并發(fā)數量

2.dispatch_semaphore_signal的源碼聲明:
dispatch_semaphore_signal

os_atomic_inc2o原子操作自增加1,然后會判斷,如果value > 0,就會返回0;
加一次后依然小于0就報異常 Unbalanced call to dispatch_semaphore_signal(),然后會調用_dispatch_semaphore_signal_slow做容錯的處理。

_dispatch_semaphore_signal_slow
_dispatch_sema4_signal
3. dispatch_semaphore_wait的源碼聲明:
dispatch_semaphore_wait
  • os_atomic_dec2o進行原子自減1操作,也就是對value值進行減操作,控制可并發(fā)數。
  • 如果可并發(fā)數為2,則調用該方法后,變?yōu)?,表示現在并發(fā)數為 1,剩下還可同時執(zhí)行1個任務,不會執(zhí)行_dispatch_semaphore_wait_slow去等待。
  • 如果初始值是0,減操作之后為負數,則會調用_dispatch_semaphore_wait_slow方法。

看看_dispatch_semaphore_wait_slow的等待邏輯

_dispatch_semaphore_wait_slow
_dispatch_sema4_wait

一個do-while循環(huán),當不滿足條件時,會一直循環(huán)下去,從而導致流程的阻塞。
上面舉例里面就相當于,下圖中的情況:

舉例

信號量總結:
1.dispatch_semaphore_wait 信號量等待,內部是對并發(fā)數做自減1操作,如果小于0,會執(zhí)行_dispatch_semaphore_wait_slow然后調用_dispatch_sema4_wait是一個do-while,直到滿足條件結束循環(huán)。
2.dispatch_semaphore_signal 信號量釋放 ,內部是對并發(fā)數做自加1操作,直到大于0時,為可操作。
3.保持線程同步,將異步執(zhí)行任務轉換為同步執(zhí)行任務。
4.保證線程安全,為線程加鎖,相當于自旋鎖。

五、dispatch_source

  • dispatch_source_create 創(chuàng)建源
  • dispatch_source_set_event_handler 設置源事件回調
  • dispatch_source_merge_data 源事件設置數據
  • dispatch_source_get_data 獲取源事件數據
  • dispatch_resume 繼續(xù)
  • dispatch_suspend 掛起
  • dispatch_source_cancel 取消源事件

定時器監(jiān)聽
倒計時案例:

- (void)iTimer {
    __block int timeout = 60;
    
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_source_t _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    dispatch_source_set_timer(_timer,dispatch_walltime(NULL, 0),1.0*NSEC_PER_SEC, 0);
    dispatch_source_set_event_handler(_timer, ^{
        if(timeout <= 0) {
            dispatch_source_cancel(_timer);
        } else {
            timeout--;
            NSLog(@"倒計時:%d", timeout);
        }
    });
    dispatch_resume(_timer);
}

自定義事件,變量增加
變量增加案例:

#import "ViewController.h"

@interface ViewController ()

@property (weak, nonatomic) IBOutlet UIButton *iBt;
@property (weak, nonatomic) IBOutlet UIProgressView *iProgress;
@property (nonatomic, strong) dispatch_source_t source;
@property (nonatomic, strong) dispatch_queue_t queue;
@property (nonatomic, assign) NSUInteger totalComplete;
@property (nonatomic ,assign) int iNum;

@end

@implementation ViewController


- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.totalComplete = 0;
    self.queue = dispatch_queue_create("lg", DISPATCH_QUEUE_SERIAL);
    self.source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());
    dispatch_source_set_event_handler(self.source, ^{
        NSUInteger value = dispatch_source_get_data(self.source); // 每次去獲取iNum的值
        self.totalComplete += value;
        NSLog(@"進度: %.2f",self.totalComplete/100.0);
        self.iProgress.progress = self.totalComplete/100.0;
    });
    
//    [self iTimer];
}

- (IBAction)btClick:(id)sender {
    if ([self.iBt.titleLabel.text isEqualToString:@"開始"]) {
        dispatch_resume(self.source);
        NSLog(@"開始了");
        self.iNum = 1;
        [sender setTitle:@"暫停" forState:UIControlStateNormal];
        
        for (int i= 0; i<1000; i++) {
            dispatch_async(self.queue, ^{
                sleep(1);
                dispatch_source_merge_data(self.source, self.iNum); // 傳遞iNum觸發(fā)hander
            });
        }
    } else {
        dispatch_suspend(self.source);
        NSLog(@"暫停了");
        self.iNum = 0;
        [sender setTitle:@"開始" forState:UIControlStateNormal];
    }
}
@end

附上dispatch_source的Demo

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容