本文主要介绍iOS开发中实现多线程技术的第二种方案:GCD(Grand Central Dispatch)
基本概念:
GCD:全称是Grand Central Dispatch,可译为“*调度器”。它是纯C语言的,提供了非常多强大的函数
GCD的优势:
GCD是苹果公司为多核的并行运算提出的解决方案
GCD会自动利用更多的CPU内核(比如双核、四核)
GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码
GCD的两个核心概念:
1. 任务:执行什么操作
2. 队列:用来存放任务
下面是进程、线程、队列和任务的关系(只是个人观点)
GCD的使用步骤:
1. 指定任务:确定想要做的事情
2. 将任务添加到队列中:
- GCD会自动将队列中的任务取出,放到对应的线程中执行
- 任务的取出遵循队列的FIFO原则:先进先出,后进后出
执行任务:
GCD中有2个用来执行任务的函数
1. 用同步的方式来执行任务
dispatch_sync(dispatch_queue_tqueue, dispatch_block_t block);
参数:queue:队列
block:任务
2. 用异步的方式来执行任务
dispatch_async(dispatch_queue_tqueue, dispatch_block_t block);
参数:同上
同步和异步的区别:
1. 同步:当前线程中执行
2. 异步:在另一条线程中执行
GCD的队列可以分为2大类型
1. 并发队列(ConcurrentDispatch Queue)
可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)
注意:并发功能只有在异步(dispatch_async)函数下才有效
2. 串行队列(Serial DispatchQueue)
让任务一个接着一个地执行(一个任务执行完毕后,才能执行下一个任务)
注意:串行队列不管在同步函数中还是异步函数中都是让任务一个接着一个执行的
说到这里是不是对任务的执行有点混淆了,下面总结一下这4个术语:同步、异步、并发、串行
1. 同步和异步决定了要不要开启新的线程
同步:在当前线程中执行任务,不具备开启新线程的能力
异步:在新的线程中执行任务,具备开启新线程的能力
2. 并发和串行决定了任务的执行方式
并发:多个任务并发(同时)执行
串行:一个任务执行完毕后,再执行下一个任务
了解这些概念之后,我们就来些代码创建队列,执行任务。
并发队列
GCD默认已经提供了全局的并发队列,供整个应用使用,不需要手动创建。使用dispatch_get_global_queue函数获得全局的并发队列
获得并发队列:
dispatch_queue_t dispatch_get_global_queue( dispatch_queue_priority_t priority, // 队列的优先级 unsigned long flags); // 此参数暂时无用,用0即可 // 队列优先级 // #defineDISPATCH_QUEUE_PRIORITY_HIGH 2 // 高 // #define DISPATCH_QUEUE_PRIORITY_DEFAULT 0// 默认(中) // #defineDISPATCH_QUEUE_PRIORITY_LOW (-2) // 低 // #define DISPATCH_QUEUE_PRIORITY_BACKGROUNDINT16_MIN // 后台如:
dispatch_queue_t queue =dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); // 获得全局并发队列
例子1:点击控制器view,打印当前线程,并添加任务到并发队列中执行。( 使用异步函数执行并发队列中的任务)- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { NSLog(@"当前线程-----%@", [NSThread currentThread]); // 获得全局并发队列 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); // 向并发队列中添加任务,按异步方式执行任务(添加3个任务) dispatch_async(queue, ^{ NSLog(@"---任务1---%@", [NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"---任务2---%@", [NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"---任务3---%@", [NSThread currentThread]); }); }
打印结果:
由于使用异步函数来执行任务,异步函数具备开启新线程的能力,它会再另一条线程中执行任务,而且任务是放在并发队列中的,任务需要并发(同时)执行。需要开启多条线程,所以开启3条线程执行任务。
例子2:点击控制器view,打印当前线程,并添加任务到并发队列中执行。(使用同步函数执行并发队列中的任务)
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { NSLog(@"当前线程-----%@", [NSThread currentThread]); // 获得全局并发队列 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); // 向并发队列中添加任务,按同步方式执行任务(添加3个任务) dispatch_sync(queue, ^{ NSLog(@"---任务1---%@", [NSThread currentThread]); }); dispatch_sync(queue, ^{ NSLog(@"---任务2---%@", [NSThread currentThread]); }); dispatch_sync(queue, ^{ NSLog(@"---任务3---%@", [NSThread currentThread]); }); }打印结果:
由于使用同步函数来执行任务,同步函数不具备开启新线程的能力,尽管任务是放在并发队列中的。任务仍然只会在当前线程(number = 1的线程,即主线程)中执行。
串行队列:
GCD中会的串行队列有2种方式
1. 使用dispatch_queue_create函数创建串行队列
dispatch_queue_t
dispatch_queue_create(constchar *label, // 队列名称
dispatch_queue_attr_tattr); // 队列属性,一般用NULL即可
如:
dispatch_queue_tqueue = dispatch_queue_create("queueName", NULL); // 创建
dispatch_release(queue);// 非ARC需要释放手动创建的队列
2. 使用主队列
- 是GCD自带的一种特殊的串行队列
- 放在主队列中的任务,都会放到主线程中执行
- 使用dispatch_get_main_queue()获得主队列
如:
dispatch_queue_tqueue = dispatch_get_main_queue();
例子1:点击控制器view,打印当前线程,并添加任务到串行队列中执行。(使用异步函数执行串行队列中的任务)
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { NSLog(@"当前线程-----%@", [NSThread currentThread]); // 创建串行队列 dispatch_queue_t queue = dispatch_queue_create("queueName", NULL); // 向串行队列中添加任务,按异步方式执行任务(添加3个任务) dispatch_async(queue, ^{ NSLog(@"---任务1---%@", [NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"---任务2---%@", [NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"---任务3---%@", [NSThread currentThread]); }); }打印结果:
由于使用异步函数来执行任务,异步函数具备开启新线程的能力,而且任务是放在串行队列中的,任务需要一个接着一个地执行,只需要在当前新线程中执行,所以开启1条线程执行任务。
例子2:点击控制器view,打印当前线程,并添加任务到串行队列中执行。(使用同步函数执行串行队列中的任务)
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { NSLog(@"当前线程-----%@", [NSThread currentThread]); // 创建串行队列 dispatch_queue_t queue = dispatch_queue_create("queueName", NULL); // 向串行队列中添加任务,按同步方式执行任务(添加3个任务) dispatch_sync(queue, ^{ NSLog(@"---任务1---%@", [NSThread currentThread]); }); dispatch_sync(queue, ^{ NSLog(@"---任务2---%@", [NSThread currentThread]); }); dispatch_sync(queue, ^{ NSLog(@"---任务3---%@", [NSThread currentThread]); }); }打印结果:
由于使用同步函数来执行任务,同步函数不具备开启新线程的能力,所以不会开启新线程执行任务。
例子3:点击控制器view,打印当前线程,并添加任务到主队列中执行。(使用异步函数执行主队列中的任务)
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { NSLog(@"当前线程-----%@", [NSThread currentThread]); // 获得主队列 dispatch_queue_t queue = dispatch_get_main_queue(); // 向主队列中添加任务,按异步方式执行任务(添加3个任务) dispatch_async(queue, ^{ NSLog(@"---任务1---%@", [NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"---任务2---%@", [NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"---任务3---%@", [NSThread currentThread]); }); }打印结果:
由于将任务添加到主队列中,而主队列的任务会默认添加到主线程中执行,主线程的任务只能一个接着一个的执行,故不会开启新线程,只在主线程中执行。
例子4:点击控制器view,打印当前线程,并添加任务到主队列中执行。(使用同步函数执行主队列中的任务)
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { NSLog(@"当前线程-----%@", [NSThread currentThread]); // 获得主队列 dispatch_queue_t queue = dispatch_get_main_queue(); // 向主队列中添加任务,按同步方式执行任务(添加3个任务) dispatch_sync(queue, ^{ NSLog(@"---任务1---%@", [NSThread currentThread]); }); dispatch_sync(queue, ^{ NSLog(@"---任务2---%@", [NSThread currentThread]); }); dispatch_sync(queue, ^{ NSLog(@"---任务3---%@", [NSThread currentThread]); }); }
运行结果:程序崩溃了
原因:使用同步函数执行主队列中的任务的时候,主队列中得任务会默认添加到主线程中去,再一个接着一个的运行。当点击控制器view的时候,主线程执行touchesBegin方法,只有当touchesBegin方法执行完毕后,才会执行任务1;而任务1要想执行就要等touchesBegin方法执行完毕,就这样相互等待下去,导致程序崩溃了。线程间通信:
由于耗时的操作都在子线程中完成,当完成之后需要显示数据\刷新UI界面的时候就必须回到主线程,这样就得从子线程回到主线程。两个线程就要相互通信。
dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{ // 执行耗时的异步操作... dispatch_async(dispatch_get_main_queue(), ^{ // 回到主线程,执行UI刷新操作 }); });
GCD的其他用法
1. 延时执行
iOS常见的延时执行有2种方式
调用NSObject的方法
[selfperformSelector:@selector(test) withObject:nil afterDelay:2.0]; // 2秒后再调用self的test方法
使用GCD函数
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,(int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ // 2秒后<span style="color:#FF0000;">异步</span>执行这里的代码.. });
如:
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ NSLog(@"当前线程-----%@", [NSThread currentThread]); // 创建串行队列 dispatch_queue_t queue =dispatch_queue_create("queue", NULL); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 *NSEC_PER_SEC)), queue, ^{ NSLog(@"---打印-- %@", [NSThread currentThread]); }); }
结果:
1. 一次性代码
使用dispatch_once函数能保证某段代码在程序运行过程中只被执行1次
staticdispatch_once_t onceToken; dispatch_once(&onceToken,^{ // 只执行1次的代码(这里面默认是线程安全的) });
2. 队列组
有这么一个需求
首先:分别异步执行2个耗时的操作
其次:等2个异步操作都执行完毕后,再回到主线程执行操作
如果想要快速高效地实现上述需求,可以考虑用队列组
dispatch_group_tgroup = dispatch_group_create(); dispatch_group_async(group,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // 执行1个耗时的异步操作 }); dispatch_group_async(group,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // 执行1个耗时的异步操作 }); dispatch_group_notify(group,dispatch_get_main_queue(), ^{ // 等前面的异步操作都执行完毕后,回到主线程... });例子:
- (void)viewDidLoad { [super viewDidLoad]; // 需求:两张图片必须全部下载完毕,才能显示数据\刷新UI界面(使用队列组方式) // 创建队列组 dispatch_group_t group = dispatch_group_create(); // 获得全局并发队列 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); // 将任务添加到并发队列中,再将并发队列添加到队列组中,队列组异步(dispatch_group_async)执行任务 dispatch_group_async(group, queue, ^{ NSLog(@"下载图片1 - %@", [NSThread currentThread]); }); dispatch_group_async(group, queue, ^{ NSLog(@"下载图片2 - %@", [NSThread currentThread]); }); // 当两张图片全部下载完毕后,通知回到主线程,显示数据\刷新UI界面 dispatch_group_notify(group, dispatch_get_main_queue(), ^{ NSLog(@"两张图片已经全部下载完毕"); }); }运行结果:
以上是GCD实现多线程浅显的讲解。由于GCD是较接近底层,且是纯C语言实现的,要想了解它的原理可能需要很长的路要走。