GCD简介
Grand Central Dispatch(GCD)是异步执行任务的技术之一。一般将应用程序中记述的线程管理用的代码在系统级中实现。开发者只需要定义想执行的任务并追加到适当的Dispatch Queue中,GCD就能生成必要的线程并计划执行任务。由于线程管理是作为系统的一部分来实现的,因此可统一管理,也可执行任务,这样就比以前的线程更有效率。
基本概念
- 进程:进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,在iOS系统中,开启一个应用就打开了一个进程。
- 线程:线程(Thread)是进程中的一个实体,程序执行的基本单元。在iOS系统中,一个进程包含一个主线程,它的主要任务是处理UI事件,显示和刷新UI。
- 同步:在当前线程依次执行,不开启新的线程。
- 异步:多个任务情况下,一个任务A正在执行,同时可以执行另一个任务B。任务B不用等待任务A结束才执行。存在多条线程。
- 队列:存放任务的结构。
- 串行:线程执行只能依次逐一先后有序的执行。
- 并发:指两个或多个事件在同一时间间隔内发生。可以在某条线程和其他线程之间反复多次进行上下文切换,看上去就好像一个CPU能够并且执行多个线程一样。其实是伪异步。
- 并行:指两个或多个事件在同一时刻发生。多核CUP同时开启多条线程供多个任务同时执行,互不干扰。
iOS中三种多线程编程技术
1.NSThread
优点:NSThread 比其他两个轻量级
缺点:需要自己管理线程的生命周期,线程同步。线程同步对数据的加锁会有一定的系统开销
2.NSOperation
优点:不需要关心线程管理,数据同步的事情,可以把精力放在自己需要执行的操作上。
3.GCD
是 Apple 开发的一个多核编程的解决方法,简单易用,效率高,速度快。通过 GCD,开发者只需要向队列中添加一段代码块(block或C函数指针),而不需要直接和线程打交道。GCD在后端管理着一个线程池,它不仅决定着你的代码块将在哪个线程被执行,还根据可用的系统资源对这些线程进行管理。这样通过GCD来管理线程,从而解决线程被创建的问题。
GCD中的队列类型
主线程串行队列,全局并发队列,自定义队列
并行队列 和 串行队列
-
队列本质:用于控制任务的执行方式
-
并行队列
- 英文:Concurrent Dispatch Queue
- 可以让多个任务并发执行,以提高执行效率
- 并发功能仅在异步(dispatch_async)函数下才有效
-
串行队列
- 英文:Serial Dispatch Queue
- 在当前线程中让任务一个接着一个地执行
-
创建队列 dispatch_queue_t
// 第一个参数:队列名称 // 第二个参数:队列类型 dispatch_queue_create(const char *label, dispatch_queue_attr_t attr); /** 队列类型 // 串行队列标识:本质就是NULL,但建议不要写成NULL,可读性不好 DISPATCH_QUEUE_SERIAL // 并行队列标识 DISPATCH_QUEUE_CONCURRENT */ 复制代码
-
创建并行队列的两种方式
-
直接创建 dispatch_queue_t queue = dispatch_queue_create("yanhooQueue", DISPATCH_QUEUE_CONCURRENT);
-
获取全局并发队列 // 第一个参数:队列优先级 // 第二个参数:保留参数,暂时无用,用0即可 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 全局并发队列的优先级 #define DISPATCH_QUEUE_PRIORITY_HIGH 2 // 高 #define DISPATCH_QUEUE_PRIORITY_DEFAULT 0 // 默认(中) #define DISPATCH_QUEUE_PRIORITY_LOW (-2) // 低 #define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN // 后台 复制代码
-
-
创建串行队列的两种方式
- 直接创建 // 创建串行队列(队列类型传递DISPATCH_QUEUE_SERIAL或者NULL) dispatch_queue_t queue = dispatch_queue_create("yanhooQueue", DISPATCH_QUEUE_SERIAL);
- 获取主队列:主队列是一种特殊的串行队列 // 主队列中的任务,都会放到主线程中执行 dispatch_queue_t queue = dispatch_get_main_queue();
同步(sync)函数 和 异步(async)函数
- 函数作用:将任务添加到队列中
- 函数类型:决定是否有开启新线程的能力
- 同步函数:不具备开启新线程的能力,只能在当前线程中执行任务 // queue:队列 // block:任务 dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
- 异步函数:具备开启线程的能力,但不一定开启新线程,比如:当前队列为主队列,异步函数也不会开启新的线程 // queue:队列 // block:任务 dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
- 经验总结:
- 通过异步函数添加任务到队列中,任务不会立即执行
- 通过同步函数添加任务到队列中,任务会立即执行
函数和队列组合后的执行效果
异步函数 + 并行队列
-
开启多条子线程,任务并行执行 // 1.创建并行队列 // dispatch_queue_t queue = dispatch_queue_create("yanhooQueue", DISPATCH_QUEUE_CONCURRENT); dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 2.通过异步函数将将任务加入队列 dispatch_async(queue, ^{ for (NSInteger i = 0; i<10; i++) { NSLog(@"1-----%@", [NSThread currentThread]); } }); dispatch_async(queue, ^{ for (NSInteger i = 0; i<10; i++) { NSLog(@"2-----%@", [NSThread currentThread]); } }); dispatch_async(queue, ^{ for (NSInteger i = 0; i<10; i++) { NSLog(@"3-----%@", [NSThread currentThread]); } }); // 证明:异步函数添加任务到队列中,任务【不会】立即执行 NSLog(@"asyncConcurrent--------end"); // 释放队列,ARC中无需也不允许调用这个方法 // dispatch_release(queue); 复制代码
异步函数 + 串行队列
-
开启一条子线程,任务是有序的在子线程上执行 // 1.创建串行队列 dispatch_queue_t queue = dispatch_queue_create("yanhooQueue", DISPATCH_QUEUE_SERIAL); // dispatch_queue_t queue = dispatch_queue_create("yanhooQueue", NULL);
// 2.通过异步函数将任务加入队列 dispatch_async(queue, ^{ NSLog(@"1-----%@", [NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"2-----%@", [NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"3-----%@", [NSThread currentThread]); }); // 证明:异步函数添加任务到队列中,任务【不会】立马执行 NSLog(@"asyncConcurrent--------end"); 复制代码
异步函数 + 主队列
-
不开启子线程,任务是在主线程中有序执行 dispatch_queue_t queue = dispatch_get_main_queue();
// 2.通过异步函数将任务加入队列 dispatch_async(queue, ^{ NSLog(@"1-----%@", [NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"2-----%@", [NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"3-----%@", [NSThread currentThread]); }); 复制代码
同步函数 + 并行队列
-
不会开启子线程,任务是有序执行 // 1.获得全局的并发队列 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 2.通过同步函数将任务加入队列 dispatch_sync(queue, ^{ NSLog(@"1-----%@", [NSThread currentThread]); }); dispatch_sync(queue, ^{ NSLog(@"2-----%@", [NSThread currentThread]); }); dispatch_sync(queue, ^{ NSLog(@"3-----%@", [NSThread currentThread]); }); // 证明:同步函数添加任务到队列中,任务【立马执行】 NSLog(@"syncConcurrent--------end"); 复制代码
同步函数 + 串行队列
-
不会开启线程,任务是有序执行
-
易发生死锁,使用时要注意 - (void)syncMain { // 1.创建串行队列 dispatch_queue_t queue = dispatch_queue_create("yanhooQueue", DISPATCH_QUEUE_SERIAL);
// 2.将任务加入队列 dispatch_sync(queue, ^{ NSLog(@"1-----%@", [NSThread currentThread]); }); dispatch_sync(queue, ^{ NSLog(@"2-----%@", [NSThread currentThread]); }); dispatch_sync(queue, ^{ NSLog(@"3-----%@", [NSThread currentThread]); }); } >>> 上面的用法不会发生死锁,原因分析如下: - 虽然都是在主线程上执行的,但任务在不同的队列中所以不会发生阻塞 - syncMain函数是在主队列中 - 其他的任务是在新建的串行队列中 复制代码
死锁的几种场景
-
场景一:(串行队列 + 同步任务 内嵌 同步任务) dispatch_queue_t queue = dispatch_queue_create("yanhooQueue", DISPATCH_QUEUE_SERIAL); dispatch_sync(queue, ^{ NSLog(@"1-----%@", [NSThread currentThread]);
// 这里阻塞了 dispatch_sync(queue, ^{ NSLog(@"2-----%@", [NSThread currentThread]); }); }); 复制代码
-
场景二:不可以在 同步函数 中调用 全局串行,会造成死锁 // 获得主队列 dispatch_queue_t queue = dispatch_get_main_queue();
// 这里阻塞了 dispatch_sync(queue, ^{ NSLog(@"1-----%@", [NSThread currentThread]); }); dispatch_sync(queue, ^{ NSLog(@"2-----%@", [NSThread currentThread]); }); dispatch_sync(queue, ^{ NSLog(@"3-----%@", [NSThread currentThread]); }); 复制代码
-
原因分析:
- 使用同步函数在任务执行过程中往任务所在的串行队列中添加任务就会导致任务间互相等待,造成死锁
- 别忘了同步函数添加任务到队列中,任务会立即执行,如果是异步函数就不会发生死锁
GCD实现线程间通信
// 全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 异步函数
dispatch_async(queue, ^
{
// 执行耗时的任务
coding...
// 【标记1】回到主线程,执行UI刷新操作
dispatch_async(dispatch_get_main_queue(), ^
{
coding...
// 还可以嵌套:再回到子线程做其他事情
dispatch_async(queue, ^
{
coding...
});
});
// 后续代码
coding....
});
复制代码
- 【标记1】处也可以用同步函数回到主线程,但是同步函数会导致添加的新任务立即执行,导致必须等添加到主队列的任务执行完才会继续执行,也不是不能这么用,看具体场景是否需要等待主队列的任务执行完毕才继续往后执行
GCD中其他常用函数
dispatch_once_t
- 函数作用:保证某段代码在程序运行过程中只被执行1次 static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // 只执行1次的代码(这个函数本身是【线程安全】的) });
dispatch_after和dispatch_time_t
-
函数作用:延迟将任务提交到队列中,不要理解成延迟执行任务 dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)); dispatch_after(time, queue, ^ { // 此任务被延迟提交到队列中 });
- 第一个参数一般是DISPATCH_TIME_NOW,表示从现在开始 - 第二个参数就是真正的延时时间,单位为纳秒dispatch_suspend 和 dispatch_resume 复制代码
dispatch_suspend 和 dispatch_resume
- dispatch_suspend
- 函数作用:只能挂起队列中还未执行的任务,正在运行的任务是无法挂起的
- dispatch_resume
- 函数作用:只能恢复队列中还未执行的任务
dispatch_apply
- 此函数和dispatch_sync函数一样,会等待处理结束,所以建议在dispatch_async中使用此函数
- 此函数必须结合并行队列才能发挥作用
- 函数作用:可以快速完成对顺序没有要求的集合遍历,因为执行顺序不确定 dispatch_apply(10, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(size_t index) { // 执行10次代码,会开启多条线程来执行任务,执行顺序不确定 mark:类似无序的遍历 });
dispatch_barrier_async
-
必须是并行队列,且不能使用全局的并行队列,实践证明不管用
-
函数作用:在此函数前面的任务执行完成后此函数才开始执行,在此函数后面的任务等此函数执行完成后才会执行 //【不能】使用全局并发队列 dispatch_queue_t queue = dispatch_queue_create("yanhooQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{ NSLog(@"----1-----%@", [NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"----2-----%@", [NSThread currentThread]); }); // 在它前面的任务执行结束后它才执行,在它后面的任务等它执行完成后才会执行 dispatch_barrier_async(queue, ^{ NSLog(@"----barrier-----%@", [NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"----3-----%@", [NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"----4-----%@", [NSThread currentThread]); }); 复制代码
dispatch_group
-
必须是并行队列才起作用
-
需求描述
- 现有三个任务:任务A、任务B、任务C
- 任务C需要等到任务A和任务B都完成后才执行
- 任务A和任务B执行没有先后顺序
-
创建dispatch_group_t // 创建队列组 dispatch_group_t group = dispatch_group_create();
-
添加任务分两种情况
-
自己可以控制并创建队列,使用dispatch_group_async // 省去创建group、queue代码...... dispatch_group_async(group, queue, ^{ // 添加任务A到group });
dispatch_group_async(group, queue, ^{ // 添加任务B到group }); 复制代码
-
无法控制队列,即使用的队列不是你创建的(如:AFNetworking异步添加任务),此时可以使用dispatch_group_enter,dispatch_group_leave控制任务的执行顺序 / 使用dispatch_group_enter,dispatch_group_leave可以方便的将一系列网络请求打包起来 AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
// 添加任务A到group // ---打标记--- dispatch_group_enter(group); [manager GET:@"http://www.baidu.com" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) { // do something // ---删除标记--- dispatch_group_leave(group); } failure:^(AFHTTPRequestOperation *operation, NSError *error) { // do something // ---删除标记--- dispatch_group_leave(group); }]; // 添加任务B到group类似上面的操作 复制代码
-
-
添加结束任务也分为两种情况
- dispatch_group_notify(推荐):不会阻塞当前线程,马上返回 dispatch_group_notify(group, dispatch_get_main_queue(), ^ { // do something });
- dispatch_group_wait(不推荐):阻塞当前线程,直到dispatch group中的所有任务完成才会返回 // 第二个参数是超时时间 dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
-
完整示例(自行创建控制的队列) // 创建队列组 dispatch_group_t group = dispatch_group_create();
// 获取全局并发队列 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); // 添加任务A到group dispatch_group_async(group, queue, ^{ // 添加任务A到group }); // 添加任务B到group dispatch_group_async(group, queue, ^{ // 添加任务B到group }); // 当任务A和任务B都执行完后到此来执行任务C dispatch_group_notify(group, queue, ^{ // 如果这里还有基于上面两个任务的结果继续执行一些代码,建议还是放到子线程中,等代码执行完毕后在回到主线程 // 回到主线程 dispatch_async(group, dispatch_get_main_queue(), ^{ // 执行相关UI显示代码... }); }); 复制代码
dispatch_set_context与dispatch_set_finalizer_f的配合使用
-
函数作用:为队列设置任意类型的数据,并在合适的时候取出来用
-
函数定义 // 设置context void dispatch_set_context(dispatch_object_t object, void *context);
- 第一个参数object:一般指通过dispatch_queue_create创建的队列 - dispatch_set_context函数完成了将context绑定到指定的GCD队列上 // 获取context void* dispatch_get_context(dispatch_object_t object); - dispatch_get_context函数完成了从指定的GCD队列获取对应的context - context是一个void类型指针,学过C语言的朋友应该都知道,void类型指针可以指向任意类型,context在这里可以是任意类型的指针 复制代码
-
完整示例 @interface Data : NSObject
@property(assign, nonatomic) int number; @end @implementation Data // 便于观察对象何时被释放 - (void)dealloc { NSLog(@"Data dealloc..."); } @end ----------------------------------------------------------------------------------------- // 定义队列的finalizer函数,用于释放context内存 void cleanStaff(void *context) { // 这里用__bridge转换,不改变内存管理权 Data *data = (__bridge Data *)(context); NSLog(@"In clean, context number: %d", data.number); // 释放context的内存! CFRelease(context); } - (void)testBody { // 创建队列 dispatch_queue_t queue = dispatch_queue_create("yanhooQueue", DISPATCH_QUEUE_CONCURRENT); // 创建Data类型context数据并初始化 Data *myData = [Data new]; myData.number = 10; // 绑定context // 这里用__bridge_retained将OC对象转换为C对象,将context的内存管理权从ARC移除,交由我们自己手动释放! dispatch_set_context(queue, (__bridge_retained void *)(myData)); // 设置finalizer函数,用于在队列执行完成后释放对应context内存 dispatch_set_finalizer_f(queue, cleanStaff); dispatch_async(queue, ^ { // 获取队列的context数据 // 这里用__bridge将C对象装换为OC对象转换,并没有改变内存管理权 Data *data = (__bridge Data *)(dispatch_get_context(queue)); // 打印 NSLog(@"1: context number: %d", data.number); // 修改context保存的数据 data.number = 20; }); } 复制代码