OC底层原理二十六:GCD详解(上)
上一节我们分析了多线程的知识,本节,我们着重分析多线程中
使用最频繁
的GCD
- GCD简介
- 函数与队列四大组合(同步、异步、串行、并行)
- 性能调度耗能
- 面试题
- 线程资源共享
- 栅栏函数barrier
- 调度组 Group
- GCD单例
- 信号量 semaphore
1. GCD简介
GCD
,全称Grand Central Dispatch
(中央调度中心),纯C语言
开发,提供了很多强大的函数
。
- GCD的优势:
- GCD是苹果公司为
多核并行
运算提出的解决方案
; - GCD会
自动利用
更多的CPU内核
(比如双核、四核等); - GCD会
自动管理
线程的生命周期
(创建线程、调度任务、销毁线程)。
程序员只需要告诉GCD
想要执行的任务
,不需要
编写任何线程管理
相关代码(调度
、销毁
都不用管
)
- GCD核心: 将
任务
添加到队列
,并指定执行任务的函数
这里引申出
任务
、队列
、执行任务的函数
三个内容。我们一一进行分析
首先,我们展示一个简单示例
:
- (void)syncTest {
// 任务(block)
dispatch_block_t block = ^{
NSLog(@"hello GCD");
};
// 队列(此处串行队列)
dispatch_queue_t queue = dispatch_queue_create("ht-syncTest", DISPATCH_QUEUE_SERIAL);
// 执行任务的函数(此处异步函数)
dispatch_async(queue, block);
}
- 借助示例,我们可以很好的理解
任务
、队列
和执行任务的函数
1.1 任务
GCD的任务
是使用block
封装的函数
,没有入参
和返参
。
- 任务创建好后,等待
执行任务的函数
将其放入队列
中。
拓展:
- 执行block,需要调用
block()
,这步调用,是执行任务的函数
内部自动管理
。
后面解析dispatch源码
时,可以清楚
知道调用时机
。
1.2 队列
GCD的队列包含串行队列
和并行队列
两种。
-
串行队列:
同一时刻
只允许一个任务
执行。(类似单车道
,汽车只能一辆辆
排队通过
) -
并行队列:
同一时刻
允许多个任务
执行。(类似多车道
,同时可以多辆
汽车通过
)
1.3 执行任务的函数
执行任务的函数
包括同步函数
和异步函数
两种:
1.3.1 dispatch_sync
同步函数:
- 必须
等待
当前语句执行完毕
,才
会执行下一条
语句 -
不
会开启线程
,就在当前线程
执行block任务
1.3.2 dispatch_async
异步函数:
-
不用等待
当前语句执行完毕
,就可以执行下一条语句 -
会
开启线程执行block
任务
(在新线程
执行还是空闲
的旧线程
执行,取决
于cpu的调度
)
异步
是多线程
的代名词
多线程
的意义
,就是为了适当提高
执行效率
,开启多个线程"同时"
执行多个任务
。- 严格来说,应该是
并行异步
是多线程
的代名词。因为只有并行
,才支持多通道
(车道),才能同时
执行多个任务
。
ps: (
下文中
提到的函数
,都指代执行任务的函数
)
为了更好的理解这些概念,下面对函数与队列
四大组合一一进行案例分析
2. 函数与队列四大组合(同步、异步、串行、并行)
未命名.png- 主队列
dispatch_get_main_queue
:
- 专门用来在
主线程
上调度任务
的串行队列
-
不
会开启线程
- 如果当前主线程
正在执行
任务,需要等
当前任务执行完
,才会继续调度其他
任务。
- 全局并发队列
dispatch_get_global_queue
:
- 为了
方便
程序员的使用
,苹果提供了全局队列 (并发队列
,实现多线程
需求的快捷方式
)。 - 使用
多线程开发
时,如果对队列没有特殊要求
,可直接
使用全局队列
来执行异步任务
。)
拓展:
Q:队列有几种?image.png- (void)demo { // 串行队列 dispatch_queue_t serial = dispatch_queue_create("ht", DISPATCH_QUEUE_SERIAL); // 并行队列 dispatch_queue_t concurrent = dispatch_queue_create("ht", DISPATCH_QUEUE_CONCURRENT); // 主队列(串行队列) dispatch_queue_t mainQueue = dispatch_get_main_queue(); // 全局队列 (并行队列) dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0); NSLog(@"\n%@ \n%@ \n%@ \n%@", serial, concurrent, mainQueue, globalQueue); }
- A:只有
串行队列
和并行队列
两种。
(底层:DQF_WIDTH
为1:表示串行队列
,DQF_WIDTH
大于1: 表示并行队列
。详细底层分析,下一节会讲)
2.1 同步 + 串行 死锁
- (void)mainSyncTest{
NSLog(@"0 %@", [NSThread currentThread]);
// 等
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"1 %@", [NSThread currentThread]);
});
NSLog(@"2 %@", [NSThread currentThread]);
}
- 打印结果(
打印0
之后,崩溃
):
- 分析:
-
主队列
(main)是串行
队列,函数是sync
同步函数。属于同步函数
&串行队列
的情况 - 在打印
0
之后,dispatch_sync
同步函数将block
排队插入mainSyncTest
函数最后,等待mainSyncTest
函数执行完后再执行。 - 但是
block
没有执行,dispatch_sync
函数就等于没有完成。程序无法往下执行。 - 所以造成了
dispatch_sync
等mainSyncTest
执行完后执行block
,而mainSyncTest
却说dispatch_sync
没有执行完,我无法结束。 😂
这里有一个误区,
image.png堵塞
与打印2
无关。
真正的堵塞,是由于
dispatch_sync
内部的block
需要等mainSyncTest
全部执行完再执行,而mainSyncTest
函数需要等dispatch_sync
执行完。
- 借用一个笑话描述:
面试官
:你讲清楚
了GCD的底层原理,我就录用你
。
大牛
:你录用我
,我就给你讲
GCD的底层原理
2.2 同步 + 并行
- (void)globalSyncTest{
for (int i = 0; i<20; i++) {
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
NSLog(@"%d-%@",i,[NSThread currentThread]);
});
}
NSLog(@"hello queue");
}
image.png
-
不
会阻塞
线程,但是一次
只通过一个
。是耗时操作
。
2.3 异步 + 串行
- (void)mainAsyncTest{
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"1 %@", [NSThread currentThread]);
});
NSLog(@"2 %@", [NSThread currentThread]);
}
image.png
- 可以发现,
异步
+串行
时,异步函数内的Block
(打印1)是在mainAsyncTest
函数全部执行完后(打印了2),再在新线程
中执行block
,打印了1。
可以对比上面
2.1 同步 + 串行
阻塞死锁的现象,两者的区别是:同步 + 串行:
dispatch_sync
必须 等mainSyncTest
执行完,才将block
任务插入尾部。
dispatch_sync
必须 等block
执行完,才算完成。异步 + 串行:
1.
dispatch_async
不用等mainAsyncTest
执行完,直接将block
任务插入尾部。
dispatch_async
不用等block
执行完,只要将block
插入尾部,就算完成了。
2.3 异步 + 并行
- (void)globalSyncTest{
for (int i = 0; i<20; i++) {
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
NSLog(@"%d-%@",i,[NSThread currentThread]);
});
}
NSLog(@"hello queue");
}
image.png
- 会
开启多个线程
,执行顺序不确定
。
3. 性能调度耗能
测试代码:
- (void)dissipation {
CFAbsoluteTime time = CFAbsoluteTimeGetCurrent();
dispatch_queue_t queue = dispatch_queue_create("ht-thread", DISPATCH_QUEUE_SERIAL);
// dispatch_async(queue, ^{
// NSLog(@"异步执行");
// });
dispatch_sync(queue, ^{
NSLog(@"同步执行");
});
NSLog(@"%f", CFAbsoluteTimeGetCurrent() - time);
}
- 对比
无任何操作
、创建线程
、创建线程调用同步函数
、创建线程调用异步函数
四种情况的耗时:
-
image.png无任何操作
时,基本无耗时
-
image.png创建线程
: 耗时0.00009秒
-
image.png创建
线程且调用异步
函数: 耗时0.00040秒
-
image.png创建线程
且调用同步函数
: 耗时0.000232秒
结论:
- 每次
创建线程
,都会有时间
上的损耗
- 线程创建后,
同步执行
比异步执行
更耗时
4. 面试题
4.1 面试题一
- (void)demo{
// 串行队列
dispatch_queue_t queue = dispatch_queue_create("ht", DISPATCH_QUEUE_SERIAL);
NSLog(@"1");
// 异步函数
dispatch_async(queue, ^{
NSLog(@"2");
// 同步
dispatch_sync(queue, ^{
NSLog(@"3");
});
});
NSLog(@"5");
}
image.png
- 打印
1
、5
、2
后,崩溃
。
如果你掌握
了上面内容
,特别[图片上传中...(未命名.png-beed7-1604486756600-0)]
是我总结的同步 + 串行
与异步 + 串行
的区别熟悉了。这题就难不住你了。
分析:
image.png
4.2 面试题二
- (void)textDemo{
// 并行队列
dispatch_queue_t queue = dispatch_queue_create("ht", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"1 %@",[NSThread currentThread]);
// 异步
dispatch_async(queue, ^{
NSLog(@"2 %@",[NSThread currentThread]);
// 同步
dispatch_sync(queue, ^{
NSLog(@"3 %@",[NSThread currentThread]);
});
NSLog(@"4 %@",[NSThread currentThread]);
});
NSLog(@"5 %@",[NSThread currentThread]);
}
image.png
-
打印结果:
1
->5
->2
->3
->4
-
与
面试题一
不同,这里是DISPATCH_QUEUE_CONCURRENT
并行队列。 -
参考
2.2 同步+并行
分析,并发队列
中的dispatch_sync
同步函数不
会阻塞线程
,但是一次
只通过一个任务
。
4.3 面试题三
- (void)textDemo{
// 并行队列
dispatch_queue_t queue = dispatch_queue_create("ht", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"1");
// 耗时
dispatch_async(queue, ^{
NSLog(@"2");
dispatch_async(queue, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
}
image.png
- 打印结果:
1
->5
->2
->4
->3
4.4 面试题四
- 选出
打印顺序
可能出现
的选项:
A: 1230789
B: 1237890
C: 3120798
D: 2137890
- (void)demo{
// 并行队列
dispatch_queue_t queue = dispatch_queue_create("ht", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"1");
});
dispatch_async(queue, ^{
NSLog(@"2");
});
// 同步
dispatch_sync(queue, ^{
NSLog(@"3");
});
NSLog(@"0");
dispatch_async(queue, ^{
NSLog(@"7");
});
dispatch_async(queue, ^{
NSLog(@"8");
});
dispatch_async(queue, ^{
NSLog(@"9");
});
}
image.png
- 答案是
A
和C
分析:
- 是
并发队列
;异步 & 并发
是无序
的,所以1
和2
的打印是无序
的,7
、8
、9
的打印是无序
的;同步 & 并发
是排队
一个个任务执行
,所以0
一定在3
后面打印,7、8、9
一定在0
后面打印。满足
0
在3
后打印,7、8、9
在0
后打印。只有选项A
和C
。
5. 线程资源共享
-
多读单写:
利用串行队列
,异步函数
支持多人买票
,同步函数
限制同一时刻
仅出一张票。
@interface ViewController ()
@property (nonatomic, assign) NSInteger tickets; // 票数
@property (nonatomic, strong) dispatch_queue_t queue; // 队列
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 准备票数
_tickets = 20;
// 创建串行队列
_queue = dispatch_queue_create("ht", DISPATCH_QUEUE_SERIAL);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 第一个线程卖票
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self saleTickes];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 第二个线程卖票
[self saleTickes];
});
}
- (void)saleTickes {
while (self.tickets > 0) {
// 模拟延时
[NSThread sleepForTimeInterval:1.0];
// 苹果不推荐程序员使用互斥锁,串行队列同步任务可以达到同样的效果!
// @synchronized
// 使用串行队列,同步任务卖票
dispatch_sync(_queue, ^{
// 检查票数
if (self.tickets > 0) {
self.tickets--;
NSLog(@"还剩 %zd %@", self.tickets, [NSThread currentThread]);
} else {
NSLog(@"没有票了");
}
});
}
}
@end
6. 栅栏函数barrier
控制
任务执行顺序
,同步
。
-
dispatch_barrier_async
: 前面任务都执行完毕,才会到这里(不会堵塞线程) -
dispatch_barrier_sync
: 堵塞线程,等待前面任务都执行完毕,才放开堵塞。堵塞期间,后面的任务都被挂起等待。
重点:栅栏函数只能控制同一并发队列
-
栅栏函数
只应用在并行
队列&异步
函数中,它的作用就是在监听
多个信号(任务)
是否都完成
。
(串行
或同步
内的信号(任务)
本身就是按顺序执行
,不需要使用到栅栏函数
。)
坑点:栅栏函数为何不能使用
dispatch_get_global_queue
队列?因为
global
队列中有很多系统任务
也在执行
。 我们需要dispatch_queue_create
手动创建一个纯净
的队列
,放置自己
需要执行的任务
,再使用栅栏函数
监听任务的执行结果
。
//MARK: -ViewController
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
__block CFAbsoluteTime time = CFAbsoluteTimeGetCurrent();
// 请求token
[self requestToken:^(id value) {
// 带token
[weakSelf requestDataWithToken:value handle:^(BOOL success) {
success ? NSLog(@"成功") : NSLog(@"失败");
NSLog(@"%f", CFAbsoluteTimeGetCurrent() - time);
}];
}];
}
/** 获取token请求 */
- (void)requestToken:(void(^)(id value))successBlock{
NSLog(@"开始请求token");
[NSThread sleepForTimeInterval:1];
if (successBlock) {
successBlock(@"b2a8f8523ab41f8b4b9b2a79ff47c3f1");
}
}
/** 请求所有数据 */
- (void)requestDataWithToken: (NSString *)token handle: (void(^)(BOOL success))successBlock {
dispatch_queue_t queue = dispatch_queue_create("ht", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
[self requestHeadDataWithToken: token handle:^(id value) { NSLog(@"%@", value); }];
});
dispatch_async(queue, ^{
[self requestListDataWithToken:token handle:^(id value) { NSLog(@"%@", value); }];
});
dispatch_barrier_async(queue, ^{ successBlock(true); });
}
/** 头部数据的请求 */
- (void)requestHeadDataWithToken:(NSString *)token handle:(void(^)(id value))successBlock{
if (token.length == 0) {
NSLog(@"没有token,因为安全性无法请求数据");
return;
}
[NSThread sleepForTimeInterval:2];
if (successBlock) {
successBlock(@"我是头,都听我的");
}
}
/** 列表数据的请求 */
- (void)requestListDataWithToken:(NSString *)token handle:(void(^)(id value))successBlock{
if (token.length == 0) {
NSLog(@"没有token,因为安全性无法请求数据");
return;
}
[NSThread sleepForTimeInterval:1];
if (successBlock) {
successBlock(@"我是列表数据");
}
}
@end
7. 调度组 Group
与栅栏函数
类似,也是控制
任务的执行顺序
。
-
dispatch_group_create
创建组 -
dispatch_group_async
进组任务 (自动管理进组
和出组
) -
dispatch_group_notify
进组任务执行完毕通知 -
dispatch_group_wait
进组任务执行等待时间
-
dispatch_group_enter
进组 -
dispatch_group_leave
出组
进组
和出组
需要成对搭配
使用 -
代码案例
//MARK: -ViewController
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
__block CFAbsoluteTime time = CFAbsoluteTimeGetCurrent();
// 1. 【手动入组和出组】
[self requestToken:^(id value) {
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t concurrent = dispatch_queue_create("ht", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_enter(group);
dispatch_async(concurrent, ^{
[weakSelf requestHeadDataWithToken:value handle:^(id value) {
NSLog(@"%@",value);
dispatch_group_leave(group);
}];
});
dispatch_group_enter(group);
dispatch_async(concurrent, ^{
[weakSelf requestListDataWithToken:value handle:^(id value) {
NSLog(@"%@",value);
dispatch_group_leave(group);
}];
});
dispatch_group_notify(group, concurrent, ^{
NSLog(@"成功了");
NSLog(@"%f", CFAbsoluteTimeGetCurrent() - time);
});
}];
// // 2. 【自动入组和出组】
// [self requestToken:^(id value) {
// dispatch_group_t group = dispatch_group_create();
// dispatch_queue_t concurrent = dispatch_queue_create("ht", DISPATCH_QUEUE_CONCURRENT);
//
// dispatch_group_async(group, concurrent, ^{
// [weakSelf requestHeadDataWithToken:value handle:^(id value) {
// NSLog(@"%@",value);
// }];
// });
//
// dispatch_group_async(group, concurrent, ^{
// [weakSelf requestListDataWithToken:value handle:^(id value) {
// NSLog(@"%@",value);
// }];
// });
//
// dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// NSLog(@"成功了");
// NSLog(@"%f", CFAbsoluteTimeGetCurrent() - time);
// });
//
// }];
// // 3. 【同步函数 + 自动入组出组】
// __block NSString * token;
// dispatch_sync(dispatch_queue_create("ht", DISPATCH_QUEUE_SERIAL), ^{
// [self requestToken:^(id value) {
// token = value;
// }];
// });
//
// dispatch_group_t group = dispatch_group_create();
// dispatch_queue_t concurrent = dispatch_queue_create("ht", DISPATCH_QUEUE_CONCURRENT);
//
// dispatch_group_async(group, concurrent, ^{
// [weakSelf requestHeadDataWithToken: token handle:^(id value) {
// NSLog(@"%@",value);
// }];
// });
//
// dispatch_group_async(group, concurrent, ^{
// [weakSelf requestListDataWithToken: token handle:^(id value) {
// NSLog(@"%@",value);
// }];
// });
//
// dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// NSLog(@"成功了");
// NSLog(@"%f", CFAbsoluteTimeGetCurrent() - time);
// });
}
/** 获取token请求 */
- (void)requestToken:(void(^)(id value))successBlock{
NSLog(@"开始请求token");
[NSThread sleepForTimeInterval:1];
if (successBlock) {
successBlock(@"b2a8f8523ab41f8b4b9b2a79ff47c3f1");
}
}
/** 头部数据的请求 */
- (void)requestHeadDataWithToken:(NSString *)token handle:(void(^)(id value))successBlock{
if (token.length == 0) {
NSLog(@"没有token,因为安全性无法请求数据");
return;
}
[NSThread sleepForTimeInterval:2];
if (successBlock) {
successBlock(@"我是头,都听我的");
}
}
/** 列表数据的请求 */
- (void)requestListDataWithToken:(NSString *)token handle:(void(^)(id value))successBlock{
if (token.length == 0) {
NSLog(@"没有token,因为安全性无法请求数据");
return;
}
[NSThread sleepForTimeInterval:1];
if (successBlock) {
successBlock(@"我是列表数据");
}
}
@end
8. GCD单例
- 单例:
- 利用
static
在内存中仅一份
的特性,保证了对象的唯一性
。 - 重写
allocWithZone
的实现,让外界使用alloc
创建时,永远返回的是static
声明的对象
。
- 以下是
KCImageManger
的核心代码:
#import "KCImageManger.h"
// 保存在常量区
static id instance;
@implementation KCImageManger
/**
每次类初始化的时候进行调用
1、+load它不遵循那套继承规则。如果某个类本身没有实现+load方法,那么不管其它各级超类是否实现此方法,系统都不会调用。+load方法调用顺序是:SuperClass -->SubClass --> CategaryClass。
3、+initialize是在类或者它的子类接受第一条消息前被调用,但是在它的超类接收到initialize之后。也就是说+initialize是以懒加载的方式被调用的,如果程序一直没有给某个类或它的子类发送消息,那么这个类的+initialize方法是不会被调用的。
4、只有执行+initialize的那个线程可以操作类或类实例,其他线程都要阻塞等着+initialize执行完。
5、+initialize 本身类的调用都会执行父类和分类实现 initialize方法都会被调多次
*/
+ (void)initialize{
NSLog(@"父类");
if (instance == nil) {
instance = [[self alloc] init];
}
}
/**
配合上面 也能进行单利
*/
+ (instancetype)manager{
return instance;
}
/**
* 所有为类的对象分配空间的方法,最终都会调用到 allovWithZone 方法
* 下面这样的操作相当于锁死 该类的所有初始化方法
*/
+(instancetype)allocWithZone:(struct _NSZone *)zone{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [super allocWithZone:zone];
});
return instance;
}
/**
单利
*/
+(instancetype)shareManager{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
@end
- 测试代码:
- (void)onceDemo{
// KCImageManger *manger1 = [KCImageManger shareManager];
// KCImageManger *manger2 = [KCImageManger shareManager];
// KCImageManger *manger3 = [KCImageManger shareManager];
// KCImageManger *manger1 = [[KCImageManger alloc] init];
// KCImageManger *manger2 = [[KCImageManger alloc] init];
// KCImageManger *manger3 = [KCImageManger new];
KCImageManger *manger1 = [[KCImageManger alloc] init];
KCImageManger *manger2 = [KCImageManger manager];
KCImageManger *manger3 = [KCImageManger manager];
NSLog(@"%@---%@---%@",manger1,manger2,manger3);
}
- 打印结果:
9. 信号量 semaphore
控制GCD
的最大并发数
。(同一时刻
可进行的信号(任务)
最大个数。)
-
dispatch_semaphore_create
: 创建信号量 -
dispatch_semaphore_wait
: 信号量等待 -
dispatch_semaphore_signal
: 信号量释放
加入了信号量的等待dispatch_semaphore_wait
后,一定需要配对
加入信号量释放dispatch_semaphore_signal
,不然会crash
- 示例代码:
- (void)viewDidLoad {
[super viewDidLoad];
// 创建全局队列(并行)
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
// 设置信号量
dispatch_semaphore_t sem = dispatch_semaphore_create(2); // 最多同时执行2个任务
//任务1
dispatch_async(queue, ^{
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
sleep(1);
NSLog(@"执行任务1");
sleep(1);
NSLog(@"任务1完成");
dispatch_semaphore_signal(sem);
});
//任务2
dispatch_async(queue, ^{
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
sleep(1);
NSLog(@"执行任务2");
sleep(1);
NSLog(@"任务2完成");
dispatch_semaphore_signal(sem);
});
//任务3
dispatch_async(queue, ^{
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
sleep(1);
NSLog(@"执行任务3");
sleep(1);
NSLog(@"任务3完成");
dispatch_semaphore_signal(sem);
});
}
下一节,分析 dispatch源码