GCD
全称Grand Central Dispatch
是苹果提供的一个多核编程的解决方案,在真正意义上实现了并行
操作,而不是并发。
GCD
使用线程池
模型来执行用户提交的任务,所以它比较节约资源,不需要为每个任务都重新创建一个新的线程,GCD
不需要自行编写并行代码,而是自动进行多核的并行计算,自动管理线程的生命周期,如:使用线程池管理线程的创建和销毁,线程的调度,任务的调度等,用户只需要编写任务代码并提交即可。
GCD
中有两个比较重要的概念:任务
和队列
。
GCD的任务:
任务顾名思义就是我们需要执行的代码块,可以是一个方法也可以是一个block
,就是我们需要线程为我们完成的工作,编写完成的任务只需提交给GCD
的队列,即可自动帮我们完成任务的调度,以及线程的调度,可以很方便的以多线程的方式执行。
GCD的队列:
队列用于管理用户提交的任务,GCD的队列有两种形式,串行队列和并发队列:
串行队列: GCD底层只维护一个线程,任务只能串行依次执行。
对于串行队列
来说,GCD
每次从串行队列
的队首取一个任务交给唯一的一个线程来处理,直到前一个任务完成后,才继续从队列中取下一个任务来执行,因此,串行队列
中的任务执行严格按照提交顺序
,并且后一个任务必须等前一个任务执行完成后才可以执行。并发队列: GCD底层使用线程池维护多个线程,任务可并发执行。
对于并发队列
来说,GCD
每次从并发队列的队首取一个任务,并将这个任务按照任务调度分发给多个线程中的某一个线程,此时不需要等待其完成,如果队列中还有其他任务继续从队列中取出并分发给某一个线程来执行,由于底层由线程池
管理多个线程,每个任务的时间复杂度不同再加上线程调度的影响,后提交的任务可能先执行完成。但对于单个线程来说,只能按顺序执行,比如某个线程被安排了多个任务,那这个线程就只能按提交顺序依次执行任务。
不论是串行队列还是并发队列都使用FIFO 先进先出
的方式来管理用户提交的任务。
所以,我们在使用GCD时也就很简单了,只需要创建或获取系统队列、编写任务并提交任务到队列即可。首先举一个下载图片的栗子,这个栗子和第一篇讲解NSThread的栗子一样,但是使用GCD来实现:
//获取一个优先级默认的全局并发队列,并以异步的方式提交任务执行
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//下载图片
UIImage *image = [[UIImage alloc] initWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1509003055&di=ef9641b620fc103323df445bf796cb13&imgtype=jpg&er=1&src=http%3A%2F%2Fwscont2.apps.microsoft.com%2Fwinstore%2F1x%2Fea9a3c59-bb26-4086-b823-4a4869ffd9f2%2FScreenshot.398115.100000.jpg"]]];
//获取主队列,在主线程中更新UI,并以异步方式提交任务
dispatch_async(dispatch_get_main_queue(), ^{
self.imageView.image = image;
});
});
上面的栗子非常简单,不需要我们手动创建线程即可实现多线程的并发编程,如果现在还看不懂没关系,学完本章内容你一定会懂。
GCD创建队列和获取队列的API:
//获取当前执行该方法的队列,被废弃了,最好不要使用
dispatch_queue_t dispatch_get_current_queue(void);
/*
获取主队列,即与主线程相关联的队列
如果需要提交任务到主线程使用该方法获取主线程的主队列即可
主队列是串行队列因为只维护主线程一个线程
*/
dispatch_queue_t dispatch_get_main_queue(void);
/*
获取一个全局的并发队列
identifier指定该队列的优先级可选值有:
DISPATCH_QUEUE_PRIORITY_HIGH 2
DISPATCH_QUEUE_PRIORITY_DEFAULT 0
DISPATCH_QUEUE_PRIORITY_LOW (-2)
DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN
flags未用到传0
*/
dispatch_queue_t dispatch_get_global_queue(long identifier, unsigned long flags);
/*
创建一个队列
label 队列的名称
attr 队列的属性可选值有:
DISPATCH_QUEUE_SERIAL 创建一个串行队列
DISPATCH_QUEUE_CONCURRENT 创建一个并发队列
通过这种方式可以自己维护一个队列
*/
dispatch_queue_t dispatch_queue_create(const char *_Nullable label, dispatch_queue_attr_t _Nullable attr);
GCD的执行方式:
- 同步执行:阻塞当前线程,直到任务执行完成后,当前线程才可继续执行。
- 异步执行:不阻塞当前线程,可能使用其他线程来执行任务,不需要等待任务完成,当前线程即可立即继续执行。
API:
/*
以异步方式执行任务,不阻塞当前线程
queue 管理任务的队列,任务最终交由该队列来执行
block block形式的任务,该block返回值、形参都为void
*/
void dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
/*
同上
context 是一个void*的指针,作为work的第一个形参
work 是一个函数指针,指向返回值为void 形参为void*的函数,且形参不能为NULL,也就是说context一定要传
使用起来不方便,一般不怎么用,需要使用C函数,也可以使用OC方法通过传递IMP来执行但是会有编译警告
*/
void dispatch_async_f(dispatch_queue_t queue, void *_Nullable context, dispatch_function_t work);
/*
以同步方式执行任务,阻塞当前线程,必须等待任务完成当前线程才可继续执行
*/
void dispatch_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block);
//同上
void dispatch_sync_f(dispatch_queue_t queue, void *_Nullable context, dispatch_function_t work);
/*
以同步方式提交任务,并重复执行iterations次
iterations 迭代执行次数
queue 管理任务的队列,任务最终交由该队列来执行
block block形式的任务,该block返回值为void形参为iterations迭代次数
*/
void dispatch_apply(size_t iterations, dispatch_queue_t queue, DISPATCH_NOESCAPE void (^block)(size_t));
//同上
void dispatch_apply_f(size_t iterations, dispatch_queue_t queue, void *_Nullable context, void (*work)(void *_Nullable, size_t));
/*
以异步方式提交任务,在when时间点提交任务
queue 管理任务的队列,任务最终交由该队列来执行
block block形式的任务,该block返回值、形参都为void
*/
void dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block);
//同上
void dispatch_after_f(dispatch_time_t when, dispatch_queue_t queue, void *_Nullable context, dispatch_function_t work);
/*
以异步方式提交任务,会阻塞queue队列,但不阻塞当前线程
queue 管理任务的队列,任务最终交由该队列来执行
需要说明的是,即时使用并发队列,该队列也会被阻塞,前一个任务执行完成才能执行下一个任务
block block形式的任务,该block返回值、形参都为void
*/
void dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);
//同上
void dispatch_barrier_async_f(dispatch_queue_t queue, void *_Nullable context, dispatch_function_t work);
/*
以同步方式提交任务,会阻塞queue队列,也会阻塞当前线程
queue 管理任务的队列,任务最终交由该队列来执行
同样的,即时是并发队列该队列也会被阻塞,需要等待前一个任务完成,同时线程也会阻塞
block block形式的任务,该block返回值、形参都为void
*/
void dispatch_barrier_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block);
//同上
void dispatch_barrier_sync_f(dispatch_queue_t queue, void *_Nullable context, dispatch_function_t work);
/*
底层线程池控制block任务在整个应用的生命周期内只执行一次
predicate 实际为long类型,用于判断是否执行过
block block形式的任务,该block返回值、形参都为void
该方法常用于实现单例类,以及结合RunLoop创建一个常驻内存的线程
*/
void dispatch_once(dispatch_once_t *predicate, dispatch_block_t block);
猛的一看常用的方法就有十二种呢,但是可以发现每一类方法都提供了block任务
和function任务
两种形式,所以常用的也就七种,只是对应了block版本和函数版本。接下来先介绍最常用的同步执行和异步执行,其他的方法后文会讲。
GCD队列和执行方式的组合:
-
异步并发队列:
//手动创建了一个并发队列 dispatch_queue_t concurrentQueue = dispatch_queue_create("myConcurrentQueue", DISPATCH_QUEUE_CONCURRENT); //也可以获取全局的并发队列,效果一样 //dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); //开启异步任务,并加入并发队列中 dispatch_async(concurrentQueue, ^{ NSLog(@"Task1 %@", [NSThread currentThread]); }); dispatch_async(concurrentQueue, ^{ NSLog(@"Task2 %@", [NSThread currentThread]); }); dispatch_async(concurrentQueue, ^{ NSLog(@"Task3 %@", [NSThread currentThread]); }); dispatch_async(concurrentQueue, ^{ NSLog(@"Task4 %@", [NSThread currentThread]); });
打印结果:
2020-03-17 11:21:14.225080+0800 多线程[2918:102124] Task2 <NSThread: 0x600003a25400>{number = 9, name = (null)} 2020-03-17 11:21:14.225108+0800 多线程[2918:103405] Task1 <NSThread: 0x600003a254c0>{number = 11, name = (null)} 2020-03-17 11:21:14.225367+0800 多线程[2918:103431] Task4 <NSThread: 0x600003a20f80>{number = 13, name = (null)} 2020-03-17 11:21:14.225491+0800 多线程[2918:103407] Task3 <NSThread: 0x600003a25500>{number = 12, name = (null)}
可以看出开辟多个线程,每个任务都是在不同的线程中同时执行。
-
异步串行队列:
//手动创建了一个串行队列 dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL); //开启异步任务,并加入串行队列中 dispatch_async(serialQueue, ^{ NSLog(@"Task1 %@", [NSThread currentThread]); }); dispatch_async(serialQueue, ^{ NSLog(@"Task2 %@", [NSThread currentThread]); }); dispatch_async(serialQueue, ^{ NSLog(@"Task3 %@", [NSThread currentThread]); }); dispatch_async(serialQueue, ^{ NSLog(@"Task4 %@", [NSThread currentThread]); });
打印结果:
2020-03-17 11:27:59.970147+0800 多线程[3004:107449] Task1 <NSThread: 0x600002394780>{number = 7, name = (null)} 2020-03-17 11:27:59.970364+0800 多线程[3004:107449] Task2 <NSThread: 0x600002394780>{number = 7, name = (null)} 2020-03-17 11:27:59.970544+0800 多线程[3004:107449] Task3 <NSThread: 0x600002394780>{number = 7, name = (null)} 2020-03-17 11:27:59.970682+0800 多线程[3004:107449] Task4 <NSThread: 0x600002394780>{number = 7, name = (null)}
可以看出开辟出一个新的线程,并且所有任务都在这个线程中按照顺序执行。
-
同步并发队列:
//手动创建了一个并发队列 dispatch_queue_t concurrentQueue = dispatch_queue_create("myConcurrentQueue", DISPATCH_QUEUE_CONCURRENT); //也可以获取全局的并发队列,效果一样 //dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); //开启同步任务,并加入并发队列中 dispatch_sync(concurrentQueue, ^{ NSLog(@"Task1 %@", [NSThread currentThread]); }); dispatch_sync(concurrentQueue, ^{ NSLog(@"Task2 %@", [NSThread currentThread]); }); dispatch_sync(concurrentQueue, ^{ NSLog(@"Task3 %@", [NSThread currentThread]); }); dispatch_sync(concurrentQueue, ^{ NSLog(@"Task4 %@", [NSThread currentThread]); });
打印结果:
2020-03-17 11:32:51.963617+0800 多线程[3081:111266] Task1 <NSThread: 0x600001204c00>{number = 1, name = main} 2020-03-17 11:32:51.963813+0800 多线程[3081:111266] Task2 <NSThread: 0x600001204c00>{number = 1, name = main} 2020-03-17 11:32:51.963920+0800 多线程[3081:111266] Task3 <NSThread: 0x600001204c00>{number = 1, name = main} 2020-03-17 11:32:51.964038+0800 多线程[3081:111266] Task4 <NSThread: 0x600001204c00>{number = 1, name = main}
可以看出没有开辟新的线程,每个任务都是在当前线程中按照顺序执行。
-
同步串行队列:
//手动创建了一个串行队列 dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL); //开启同步任务,并加入串行队列中 dispatch_sync(serialQueue, ^{ NSLog(@"Task1 %@", [NSThread currentThread]); }); dispatch_sync(serialQueue, ^{ NSLog(@"Task2 %@", [NSThread currentThread]); }); dispatch_sync(serialQueue, ^{ NSLog(@"Task3 %@", [NSThread currentThread]); }); dispatch_sync(serialQueue, ^{ NSLog(@"Task4 %@", [NSThread currentThread]); });
打印结果:
2020-03-17 11:39:27.880465+0800 多线程[3162:115800] Task1 <NSThread: 0x6000020b25c0>{number = 1, name = main} 2020-03-17 11:39:27.880655+0800 多线程[3162:115800] Task2 <NSThread: 0x6000020b25c0>{number = 1, name = main} 2020-03-17 11:39:27.880791+0800 多线程[3162:115800] Task3 <NSThread: 0x6000020b25c0>{number = 1, name = main} 2020-03-17 11:39:27.880908+0800 多线程[3162:115800] Task4 <NSThread: 0x6000020b25c0>{number = 1, name = main}
可以看出没有开辟新的线程,每个任务都是在当前线程中按照顺序执行。
GCD队列和执行方式的组合总结:
type | Serial串行队列 | Concurrent并发队列 |
---|---|---|
async异步执行 | 不阻塞当前线程,使用其他线程串行执行任务,只有一个线程按顺序执行任务 | 不阻塞当前线程,并发执行任务,使用多个线程同时执行任务 |
sync同步执行 | 阻塞当前线程,使用同一线程串行执行任务,只有一个线程用于执行任务 | 阻塞当前线程,可能使用同一线程串行执行任务 |
所以,针对异步执行/同步执行和串行队列/并发队列,只需要掌握其关键就可以了,同步/异步的区别在于是否阻塞线程,串行/并发队列的区别在于有多少个线程参与执行任务。即时存在嵌套结构也能够很好理解了。举一个嵌套结构的例子:
dispatch_queue_t concurrentQueue = dispatch_queue_create("myConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t serialQueue = dispatch_queue_create("mySerialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(serialQueue, ^{
dispatch_async(concurrentQueue, ^{
for (int i = 0; i < 500; i++)
{
NSLog(@"Task1 %@ %d", [NSThread currentThread], i);
}
});
dispatch_async(concurrentQueue, ^{
for (int i = 0; i < 500; i++)
{
NSLog(@"Task2 %@ %d", [NSThread currentThread], i);
}
});
dispatch_async(concurrentQueue, ^{
for (int i = 0; i < 500; i++)
{
NSLog(@"Task3 %@ %d", [NSThread currentThread], i);
}
});
for (int i = 0; i < 100; i++)
{
NSLog(@"Complete.");
}
});
外层dispatch不论使用串行队列还是并发队列,由于只有一个任务,只会有一个线程来执行这个block块的内容,而同步和异步的区别就在于是否会阻塞当前线程,接下来看block块的内容,采用了三个异步提交到并发队列,所以并发队列里就有了三个不同的任务,就可以真正执行并发,由于都是异步提交没有阻塞当前线程,所以输出Complete
的代码也会掺杂在Task1-3
中乱序输出。
GCD其他方法的使用:
-
dispatch_apply:表示重复执行任务
iterations
次。同步执行
,会阻塞当前线程。dispatch_apply(20000, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(size_t t) { NSLog(@"Task %@ %ld", [NSThread currentThread], t); });
注意:如果该方法是在主线程中执行,则不能传入当前串行队列。因为
dispatch_apply
方法是是同步执行,又把该方法放入当前串行队列中去执行,但是当前串行队列处于阻塞中,等待dispatch_apply
方法执行完成,所以会产生死锁。 -
dispatch_after:等待
when
时间后,执行任务。异步执行
,不会造成线程阻塞。NSLog(@"Before"); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 5), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSLog(@"In %@", [NSThread currentThread]); }); NSLog(@"After");
打印结果:
2020-03-17 14:58:27.403048+0800 多线程[4687:212802] Before 2020-03-17 14:58:27.403355+0800 多线程[4687:212802] After 2020-03-17 14:58:32.403767+0800 多线程[4687:212986] In <NSThread: 0x6000007a6040>{number = 7, name = (null)}
可以看出这个5s并不是精确的5s,因为该方法是在when时间点到达的时候去提交任务到队列,所以是延迟提交,而不是延迟执行,队列什么时候安排线程去执行是未知的,所以不要用这个方法去实现定时器这样的功能。
-
dispatch_barrier_(a)sync:栅栏函数,用于阻塞当前队列,等待当前队列任务执行完成后,在执行
dispatch_barrier_(a)sync
的任务。dispatch_queue_t concurrentQueue = dispatch_queue_create("myConcurrentQueue", DISPATCH_QUEUE_CONCURRENT); dispatch_async(concurrentQueue, ^{ for (int i = 0; i < 500; i++) { NSLog(@"Task0 %@ %d", [NSThread currentThread], i); } }); dispatch_async(concurrentQueue, ^{ for (int i = 0; i < 500; i++) { NSLog(@"Task1 %@ %d", [NSThread currentThread], i); } }); dispatch_barrier_async(concurrentQueue, ^{ for (int i = 0; i < 500; i++) { NSLog(@"Task2 %@ %d", [NSThread currentThread], i); } }); dispatch_async(concurrentQueue, ^{ for (int i = 0; i < 500; i++) { NSLog(@"Task3 %@ %d", [NSThread currentThread], i); } }); dispatch_async(concurrentQueue, ^{ for (int i = 0; i < 500; i++) { NSLog(@"Task4 %@ %d", [NSThread currentThread], i); } });
上面的输出是按照
Task0
Task1
并发执行,Task2
等待Task0
Task1
执行完成后单独执行, 最后Task3
Task4
等待Task2
执行完成后开始并发执行 。dispatch_barrier_async
方法常与并发队列
共用,前一段任务使用dispatch_async
异步并发执行,然后插入一个dispatch_barrier_async
执行一个中间任务,这个中间任务必须要等待前面的并发任务执行完成后才能开始执行,接着这个中间任务完成后,继续异步并发执行接下来的任务。 -
dispatch_once:该方法能够保证在应用的生命周期内只执行一次提交的任务,所以常用于单例类的创建。
@interface MyUtil: NSObject <NSCopying> + (instancetype)sharedUtil; @end @implementation MyUtil static MyUtil *staticMyUtil = nil; + (instancetype)sharedUtil { //保证初始化创建只执行一次 static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ staticMyUtil = [[MyUtil alloc] init]; }); return staticMyUtil; } //防止通过alloc或new直接创建对象 + (instancetype)allocWithZone:(struct _NSZone *)zone { //保证alloc函数只执行一次 static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ staticMyUtil = [super allocWithZone:zone]; }); return staticMyUtil; } //实现NSCopying协议的方法,防止通过copy获取副本对象 - (instancetype)copyWithZone:(NSZone *)zone { return staticMyUtil; } @end
dispatch_once
函数需要传入一个long类型的predicate
,这个值必须是独一无二的,使用静态变量的地址最合适不过了,MyUtil
实现了NSCopying
协议的copyWithZone:
方法,防止通过copy
方法获取副本对象。当使用
alloc&&init
方法初始化时,先调用allocWithZone:
方法来分配存储空间,如果再次使用sharedUtil
方法来获取的话,由于没有执行过,会执行到dispatch_once
内部block
,此时会再去执行allocWithZone:
方法,但该方法内部dispatch_once
已经执行过了会直接返回staticMyUtil
,反过来调用是一样的道理,通过这样的方式就可以实现真正的单例了。 -
dispatch_ group_ t:是一个比较实用的方法,通过构造一个组的形式,将各个同步或异步提交任务都加入到同一个组中,当所有任务都完成后会收到通知,用于进一步处理,通过这样的方式就可以实现多线程下载,当下载完成后就可以通知用户了。
dispatch_queue_t concurrentQueue = dispatch_queue_create("myConcurrentQueue", DISPATCH_QUEUE_CONCURRENT); dispatch_group_t group = dispatch_group_create(); dispatch_group_async(group, concurrentQueue, ^{ for (int i = 0; i < 500; i++) { NSLog(@"Task1 %@ %d", [NSThread currentThread], i); } }); dispatch_group_async(group, dispatch_get_main_queue(), ^{ for (int i = 0; i < 500; i++) { NSLog(@"Task2 %@ %d", [NSThread currentThread], i); } }); dispatch_group_async(group, concurrentQueue, ^{ for (int i = 0; i < 500; i++) { NSLog(@"Task3 %@ %d", [NSThread currentThread], i); } }); dispatch_group_notify(group, concurrentQueue, ^{ NSLog(@"All Task Complete"); });
像一个组中添加了三个异步的任务,最终三个任务完成后可以收到通知执行回调的
block
,上面的输出为,All Task Complete
在前面三个输出都结束后才会输出。
防止GCD产生死锁:
- (void)viewWillAppear:(BOOL)animated
{
NSLog(@"Before");
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"In");
});
NSLog(@"After");
}
上述代码就会产生死锁,分析下原因,首先,viewWillAppear:
方法是在主线程
中执行的,接着调用dispatch_sync
方法,该方法会阻塞
当前线程,也就是会阻塞主线程
,主线程被阻塞是为了等待任务的完成,然后该代码将任务添加
到了主队列
,主队列会将任务交给主线程执行,但此时主线程阻塞
了,任务添加进了主线程得不到运行,而主线程在等待任务的执行
,因此就造成了死锁
。
这个栗子一般人写不出来这样的代码,仅仅是为了讲解什么情况下会造成死锁,即,线程被阻塞需要等到任务执行完成,而任务由于线程阻塞得不到执行。前文举的几个串行队列的栗子很多是不能使用主队列的,原因也正在此。
dispatch_apply(1, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(size_t t) {
dispatch_apply(2000, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(size_t t) {
NSLog(@"===== %@ %ld", [NSThread currentThread], t);
});
});
上述栗子也会造成死锁,因为,dispatch_apply
同样会阻塞
当前线程,它需要等待内部的dispatch_apply
执行完成,内部的需要等待外部的线程来执行它,产生了死锁。
结论:使用sync同步函数
往当前串行队列
中添加任务,会卡主当前的串行队列(产生死锁)。