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_t *val它里面有一個狀態(tài)的記錄,來保證block只被調用一次。
dispatch_once_f的源碼聲明:

需要看看執(zhí)行func的要出于什么條件下才會被執(zhí)行 (_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_f_inline里會判斷不同的隊列條件來去選擇分支繼續(xù)往下走,這里我以并發(fā)隊列為例,它會走_dispatch_sync_f_slow代碼分支:

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

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

_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)

(柵欄函數柵不住全局隊列的原因就在這里,因為它指定的wakeup函數不一樣。)
喚醒以并發(fā)隊列為例,它會走_dispatch_lane_wakeup

為barrier形式,調用_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_barrier_sync和普通同步函數dispatch_sync效果是一樣的:
阻塞當前線程,不開辟線程,立即執(zhí)行,同步柵欄函數還需要等柵欄任務完成后喚醒非全局隊列后續(xù)的任務
為什么蘋果設計柵欄函數柵不住全局并發(fā)隊列?
因為我們系統(tǒng)也會使用全局并發(fā)隊列,避免造成系統(tǒng)任務被阻塞。
2.異步柵欄dispatch_barrier_async

-
_dispatch_continuation_init保存任務

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

_dispatch_continuation_async

dx_push是宏定義:
#define dx_push(x, y, z) dx_vtable(x)->dq_push(x, y, z)
找到dq_push的聲明:

根據不同的隊列賦值給dq_push不一樣的函數
以并發(fā)隊列為例:
_dispatch_lane_concurrent_push的源碼聲明:
_dispatch_lane_concurrent_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_enter和dispatch_group_leave;效果是一樣的。
研究的對象有三個:dispatch_group_enter、dispatch_group_leave、dispatch_group_notify。
1. 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_async函數以外的方式顯式地從組中添加和刪除任務,那么使用這個函數(與dispatch_group_enter一起使用)允許您的應用程序正確地管理任務引用計數。
對該函數的調用必須平衡對dispatch_group_enter的調用。調用它的次數超過dispatch_group_enter是無效的,這會導致負的計數。
3.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_enter與dispatch_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的源碼聲明:


- 當兩個線程需要協(xié)調特定事件的完成時,為該值傳遞0很有用;
- 傳遞大于0的值對于管理有限的資源池很有用,其中池大小等于該值;
- 信號量的起始值傳遞小于信號量的起始值。
傳遞小于零的值將導致返回 NULL,也就是小于0就不會正常執(zhí)行。
總的來說:信號量初始值可以控制線程池中的最多并發(fā)數量
2.dispatch_semaphore_signal的源碼聲明:

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


3. 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的等待邏輯


一個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