本章节主要介绍多线程中使用最频繁的GCD
- GCD简介
- 函数与队列四大组合(同步、异步、串行、并行)
- 性能调度耗能
- 面试题
- 线程资源共享
- 栅栏函数barrier
- 调度组 Group
- GCD单例
- 信号量 semaphore
1 GCD简介
GCD,全称Grand Central Dispatch(中央调度中心),纯C语言开发,提供了很多强大的函数。
- 优势:
- GCD是苹果公司为多核并行运算提出的解决方案;
- GCD会自动利用更多的CPU内核;
- GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程)。
- 程序员只需要告诉GCD想要执行的任务,不需要编写任何线程管理相关代码(调度、销毁都不用管)
- 核心: 将任务添加到队列,并指定执行任务的函数
这里引申出任务、队列、执行任务的函数三个内容。我们一一进行分析
// 任务(block)
dispatch_block_t block = ^{
NSLog(@"hello GCD");
};
// 队列(此处串行队列)
dispatch_queue_t queue = dispatch_queue_create("syncTest", DISPATCH_QUEUE_SERIAL);
// 执行任务的函数(此处异步函数)
dispatch_async(queue, block);
1.1 任务
GCD的任务是使用block封装的函数,没有入参和返参。 任务创建好后,等待执行任务的函数将其放入队列中。
1.2 队列
GCD的队列包含串行队列和并行队列两种。 串行队列: 同一时刻只允许一个任务执行。 并行队列: 同一时刻允许多个任务执行。
1.3 执行任务的函数
执行任务的函数包括同步函数和异步函数两种:
- dispatch_sync同步函数:
- 必须等待当前语句执行完毕,才会执行下一条语句
- 不会开启线程,就在当前线程执行block任务
- dispatch_async异步函数:
- 不用等待当前语句执行完毕,就可以执行下一条语句
- 会开启线程执行block任务
2 函数与队列四大组合(同步、异步、串行、并行)
-
主队列dispatch_get_main_queue:
- 专门用来在主线程上调度任务的串行队列
- 不会开启线程
- 如果当前主线程正在执行任务,需要等当前任务执行完,才会继续调度其他任务。
-
全局并发队列dispatch_get_global_queue:
- 为了方便程序员的使用,苹果提供了全局队列 (并发队列,实现多线程需求的快捷方式)。
- 使用多线程开发时,如果对队列没有特殊要求,可直接使用全局队列来执行异步任务
Q & A:
Q: 队列有几种?
// 串行队列
dispatch_queue_t serial = dispatch_queue_create("ypy", DISPATCH_QUEUE_SERIAL);
// 并行队列
dispatch_queue_t concurrent = dispatch_queue_create("ypy", 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:只有串行队列和并行队列两种。
2.1 同步 + 串行 死锁
- (void)mainSyncTest{
NSLog(@"0 %@", [NSThread currentThread]);
// 等
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"1 %@", [NSThread currentThread]);
});
NSLog(@"2 %@", [NSThread currentThread]);
}
分析:
- 主队列(main)是串行队列,函数是sync同步函数。属于同步函数&串行队列的情况
- 在打印0之后,dispatch_sync同步函数将block排队插入mainSyncTest函数最后,等待mainSyncTest函数执行完后再执行。
- 但是block没有执行,dispatch_sync函数就等于没有完成。程序无法往下执行。
- 所以造成了dispatch_sync等mainSyncTest执行完后执行block,而mainSyncTest却说dispatch_sync没有执行完.
Q1 :如果上述代码,将同步 + 主队列执行改为同步 + 自定义串行队列,是否会堵塞?
dispatch_queue_t serial = dispatch_queue_create("ypy", DISPATCH_QUEUE_SERIAL);
NSLog(@"0 %@", [NSThread currentThread]);
dispatch_sync(serial, ^{
NSLog(@"1 %@", [NSThread currentThread]);
});
NSLog(@"2 %@", [NSThread currentThread]);
总结 虽然将主队列执行改为自定义串行队列,解决了堵塞问题,但是否和syncTest代码本身运行的队列和线程有关呢? [之前堵塞代码] 函数代码和block代码都在主队列+主线程 [新代码]函数代码在自定义队列+主线程
Q2: 将syncTest代码也放在这个自定义队列中执行,此时堵塞是否会出现?
dispatch_queue_t serial = dispatch_queue_create("ypy", DISPATCH_QUEUE_SERIAL);
dispatch_sync(serial, ^{
[self mainSyncTest];
});
- (void)mainSyncTest{
NSLog(@"0 %@", [NSThread currentThread]);
// 等
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"1 %@", [NSThread currentThread]);
});
NSLog(@"2 %@", [NSThread currentThread]);
}
总结 我们注意到线程还是在main主线程。所以:同一队列同一线程进行sync同步操作,会阻塞,Crash 主队列是个特殊队列,APP启动时就与主线程完成了线程绑定。不会切换线程。
Q3 :自定义队列切其他自定义队列,是否也可以阻止阻塞呢? Q4 :是否和主线程有关?如果当前函数在子线程执行,任务回归主线程操作,是否也可以阻止阻塞呢?
总结 我们注意到线程还是在main主线程。所以:同一队列同一线程进行sync同步操作,会阻塞,Crash
继续探索,按照上面说的,自定义队列切主队列,是否也可以阻止阻塞呢? 总结: 主队列是个特殊队列,APP启动时就与主线程完成了线程绑定,不会切换线程。
- 当前环境:主队列+主线程,执行sync同步 + main_queue主队列任务,会阻塞
- 当前环境:主队列+主线程,切换到自定义串行队列,不会开辟线程(block在main线程执行),不会阻塞
- 当前环境:自定义串行队列+主线程,切换到主队列,会阻塞
- 当前环境:自定义串行队列+主线程,切换到新自定义串行队列,不会开辟线程(block在main线程执行),不会阻塞
- 当前环境:自定义串行队列+子线程,切换到主队列,主队列是绑定main线程的,所以会切换回main线程执行block任务,执行完后回到子线程,执行后续任务。不会阻塞
2.2 同步 + 并行
for (int i = 0; i<20; i++) {
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
NSLog(@"%d-%@",i,[NSThread currentThread]);
});
}
NSLog(@"hello queue");
总结 不会阻塞线程,但是一次只通过一个。是耗时操作。
2.3 异步 + 串行
- (void)mainAsyncTest{
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"1 %@", [NSThread currentThread]);
});
NSLog(@"2 %@", [NSThread currentThread]);
}
可以发现,异步+串行时,异步函数内的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.4 异步 + 并行
for (int i = 0; i<20; i++) {
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
NSLog(@"%d-%@",i,[NSThread currentThread]);
});
}
NSLog(@"hello queue");
总结 会开启多个线程,执行顺序不确定。
3. 性能调度耗能
CFAbsoluteTime time = CFAbsoluteTimeGetCurrent();
dispatch_queue_t queue = dispatch_queue_create("thread", DISPATCH_QUEUE_SERIAL);
// dispatch_async(queue, ^{
// NSLog(@"异步执行");
// });
dispatch_sync(queue, ^{
NSLog(@"同步执行");
});
NSLog(@"%f", CFAbsoluteTimeGetCurrent() - time);
对比无任何操作、创建线程、创建线程调用同步函数、创建线程调用异步函数四种情况的耗时:
- 无任何操作时,基本无耗时
CFAbsoluteTime time = CFAbsoluteTimeGetCurrent();
NSLog(@"%f", CFAbsoluteTimeGetCurrent() - time);
- 创建线程: 耗时0.00009秒
CFAbsoluteTime time = CFAbsoluteTimeGetCurrent();
dispatch_queue_t queue = dispatch_queue_create("thread",
NSLog(@"%f", CFAbsoluteTimeGetCurrent() - time);
- 创建线程且调用异步函数: 耗时0.00040秒
CFAbsoluteTime time = CFAbsoluteTimeGetCurrent();
dispatch_queue_t queue = dispatch_queue_create("thread", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
NSLog(@"异步执行");
});
NSLog(@"%f", CFAbsoluteTimeGetCurrent() - time);
- 创建线程且调用同步函数: 耗时0.000232秒
CFAbsoluteTime time = CFAbsoluteTimeGetCurrent();
dispatch_queue_t queue = dispatch_queue_create("thread", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
NSLog(@"同步执行");
});
NSLog(@"%f", CFAbsoluteTimeGetCurrent() - time);
总结:
- 每次创建线程,都会有时间上的损耗
- 线程创建后,同步执行比异步执行更耗时
4. 面试题
面试题1:
// 串行队列
dispatch_queue_t queue = dispatch_queue_create("ypy", DISPATCH_QUEUE_SERIAL);
NSLog(@"1");
// 异步函数
dispatch_async(queue, ^{
NSLog(@"2");
// 同步
dispatch_sync(queue, ^{
NSLog(@"3");
});
});
NSLog(@"5");
答案: 打印 1、5、2后,崩溃
面试题2 :
// 并行队列
dispatch_queue_t queue = dispatch_queue_create("ypy", 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]);
答案
- 打印结果: 1 -> 5 -> 2 -> 3 -> 4
- 与面试题一不同,这里是DISPATCH_QUEUE_CONCURRENT并行队列。
- 参考2.2 同步+并行分析,并发队列中的dispatch_sync同步函数不会阻塞线程,但是一次只通过一个任务。
面试题3:
// 并行队列
dispatch_queue_t queue = dispatch_queue_create("ypy", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"1");
// 耗时
dispatch_async(queue, ^{
NSLog(@"2");
dispatch_async(queue, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
答案: 打印结果: 1 -> 5 -> 2 -> 4 -> 3
面试题4:
选出打印顺序可能出现的选项: A: 1230789 B: 1237890 C: 3120798 D: 2137890
// 并行队列
dispatch_queue_t queue = dispatch_queue_create("ypy", 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");
});
答案 是 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("ypy", 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手动创建一个纯净的队列,放置自己需要执行的任务,再使用栅栏函数监听任务的执行结果。
@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("ypy", 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 出组 进组和出组需要成对搭配使用
@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("ypy", 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);
});
}];
}
/** 获取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
- GCD单例 单例: 利用static在内存中仅一份的特性,保证了对象的唯一性。 重写allocWithZone的实现,让外界使用alloc创建时,永远返回的是static声明的对象。 以下是KCImageManger的核心代码:
// 保存在常量区
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
9 信号量 semaphore
控制GCD的最大并发数。(同一时刻可进行的信号(任务)最大个数。)
- dispatch_semaphore_create: 创建信号量
- dispatch_semaphore_wait: 信号量等待
- dispatch_semaphore_signal: 信号量释放
加入了信号量的等待dispatch_semaphore_wait后,一定需要配对加入信号量释放dispatch_semaphore_signal,不然会crash
// 创建全局队列(并行)
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);
});