第二篇:GCD
目录
一、dispatch_async和dispatch_sync
二、串行队列和并行队列、主队列和全局队列
三、dispatch_async、dispatch_sync和串行队列、并行队列的四种组合举例,来验证一下前两部分的理论
四、死锁
五、GCD的其它常用API
1、dispatch_once
2、dispatch_after
3、dispatch_group
4、dispatch_barrier_async
5、dispatch_apply
前面一篇我们说到NSThread是面向线程来实现多线程编程的,需要我们自己来写一些线程管理的代码,这样的话一方面我们开发起来代码量会多不简洁,另一方面这样的线程管理效率并不高,再者万一我们有100个任务要求并行执行或者说并发执行,那NSThread是没有办法重用之前的线程的,因此就需要创建100个线程,这太可怕了,GCD可以重用线程。
而GCD则提供了十分简洁的多线程实现方案,API十分简洁,只需要把我们想要执行的任务放在一个block里,并分配到指定的队列,系统就会帮我们创建线程实现多线程开发,也就是说线程的管理交给了系统内核来做,换句话说无论我们手动如何编写线程管理代码也比不上系统内核来管理,因此GCD的执行效率要比NSThread高很多。此外GCD也提供了一些其它非常有用的API来辅助我们完成多线程的开发。
一、dispatch_async和dispatch_sync
当我们看到dispatch_async和dispatch_sync时,我们只需要从两方面分析它俩就可以了,即
能否开辟线程和是否会阻塞当前线程
。dispatch_async能开辟线程,而且不会阻塞当前线程。
dispatch_sync不能开辟线程,而且会阻塞当前线程。
因此,抓住dispatch_async和dispatch_sync的这两个特点,我们知道
如果我们想用GCD实现多线程开发,就必须使用dispatch_async来分配任务,使用dispatch_sync是实现不了多线程开发的,
现在我暂时不知道dispatch_sync存在的意义,没有找到它的实际应用场景。
下面我们来具体说一下。
1、dispatch_async
// 异步地分配任务
dispatch_async(someQueue, ^{
// 任务...
});
首先我们要知道:dispatch_async是将一个任务异步地
分配到指定的队列中,所谓异步是指这个函数在把一个任务分配到指定的队列后,会立马返回
。说得更直白一点就是dispatch_async这个函数的功能 = 把任务分配到队列中
,这个函数的功能执行完,下面的代码就能执行,因此使用dispatch_async分配任务不会阻塞调用dispatch_async函数的线程
,不阻塞的原因就是dispatch_async根本不关心它所分配的那个任务执行不执行,它分配完就完事了。
其次我们要知道:只要你使用dispatch_async来分配任务,GCD都会开辟线程来执行这些任务,
当队列是串行队列时,GCD只开辟一个线程,当队列是并行队列时,GCD开辟多个线程。也只有在使用dispatch_async分配任务的情况下,串行队里和并行队列才有明显的差别。
2、dispatch_sync
// 同步地分配任务
dispatch_sync(someQueue, ^{
// 任务...
});
首先我们要知道:dispatch_sync是将一个任务同步地
分配到指定的队列中,所谓同步是指这个函数在把一个任务分配到指定的队列后,不会立马返回,而是要挂在这儿等,等它所分配的这个任务执行完毕后才返回
。说得更直白一点就是dispatch_sync这个函数的功能 = 把任务分配到队列中 + 等待这个任务执行完毕
,这个函数的功能不执行完,下面的代码就甭想执行,因此使用dispatch_sync分配任务会阻塞调用dispatch_sync函数的线程
,阻塞的原因就是dispatch_sync函数要等它所分配的那个任务执行完毕。
其次我们要知道:只要你使用dispatch_sync来分配任务,那么不管你分配到的队列是串行队列还是并行队列,GCD都不会开辟线程,而是把队列里的任务(其实只有一个,见细想)放在调用dispatch_sync函数的线程上来执行
。细想一下,dispatch_sync是分配一个任务到队列(无论是串行队列还是并行队列)中,就立马拿来执行,执行完就从队列里移除,因此我们也就看到这种情况下队列其实是没有存在意义的,串行队列和并行队列在这种情况下没什么差别,因为dispatch_sync总是一个任务一个任务地入队列-->执行完-->出队列啊,而队列的真正目的是存储多个任务嘛。
二、串行队列和并行队列、主队列和全局队列
队列是一种数据结构,在GCD里它是用来存储任务的,它内部的任务会按照添加的顺序,先进先出地启动执行
(注意这里任务仅仅是按顺序启动执行,而不一定会按顺序执行完毕,因此下文中说到的“按顺序执行”是指“按顺序执行完毕”),而队列又可以分为串行队列和并行队列两种,此外GCD还为我们提供了两个特殊的队列主队列和全局队列。
当我们看到串行队列和并行队列时,也是只需要从两方面分析它俩就可以了,即
GCD会为队列会开辟几个线程和队列里的任务是否按顺序执行
,当然它俩的分析其实要基于我们使用的是dispatch_async还是dispatch_sync的。当我们使用dispatch_async时,GCD只会为串行队列开辟一个线程,队列里的任务按顺序执行;GCD会为并行队列会开辟多个线程,队列里的任务不按顺序执行,我们也无法控制任务的执行顺序。
当我们使用dispatch_sync时,无论是串行队列还是并行队列GCD都不会开辟线程,队列里的任务会被添加到调用dispatch_sync函数的线程里执行,当然任务是按顺序执行的。
因此,抓住串行队列和并行队列的这两个特点,我们就能根据自己实际的需求来决定到底使用串行队列还是并行队列了,
如果我们的多个任务必须按顺序执行或者我们不想让多个任务并行执行,那就必须得用串行队列;而如果我们对任务的执行顺序没有要求,那就大可以使用并行队列,毕竟并行队列是多个任务并行执行,程序的执行效率会更高一些。
下面我们来具体说一下。
1、串行队列(serial queue)
// 串行队列:第一个参数是该队列的唯一标识符,第二个参数是队列的类型
dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
首先我们要知道:GCD只会为串行队列开辟一个线程来挨个执行队列里面的任务。
其次我们要知道:串行队列里的任务是按顺序启动执行的,而且也是按顺序执行完毕的,因为串行队列里只有上一个任务启动执行并执行完毕,下一个任务才会启动执行。总的来说,串行队列里的任务是按顺序执行的。
2、并行队列(concurrent queue)
// 并行队列:第一个参数是该队列的唯一标识符,第二个参数是队列的类型
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
首先我们要知道:GCD会为并行队列开辟多个线程来同时执行队列里的任务,至于具体开辟几个,这个取决于当时CPU的核数和负荷情况。
其次我们要知道:并行队列里的任务是按顺序启动执行的,但不一定按顺序执行完毕,因为并行队列并不会等待上一个任务执行完毕后才启动下一个任务,而是在上一个任务启动之后就立马启动下一个任务,而且一旦上一个任务的工作量大于下一个任务的工作量,我们也无法保证上一个任务肯定比下一个任务先执行完毕。总的来说,并行队列里的任务不是按顺序执行的,
3、主队列(main queue)
// 主队列
dispatch_queue_t mainQueue = dispatch_get_main_queue();
主队列是一个串行队列,我们添加到主队列里的任务都会被放到主线程中去执行。(主线程创建的同时,主队列也跟着创建完毕了,并且系统自动把主线程中要执行的任务都添加到主队列中)
回到主线程举例:
dispatch_async(dispatch_get_main_queue(), ^{
// 刷新UI或做其它操作...
});
4、全局队列(global queue)
// 全局队列
dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
全局队列是一个并行队列,一般情况下如果我们要使用并行队列,直接用全局队列就可以了,没必要再专门去创建一个并行队列,除非项目中要使用到多个并行队列。
三、dispatch_async、dispatch_sync和串行队列、并行队列的四种组合举例,来验证一下前两部分的理论
1、dispatch_async + 串行队列
我们把5个任务异步地分配到1个串行队列里:
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t queue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
NSLog(@"111");
dispatch_async(queue, ^{
NSLog(@"%@", [NSThread currentThread]);
NSLog(@"1");
});
dispatch_async(queue, ^{
NSLog(@"%@", [NSThread currentThread]);
NSLog(@"2");
});
dispatch_async(queue, ^{
NSLog(@"%@", [NSThread currentThread]);
NSLog(@"3");
});
dispatch_async(queue, ^{
NSLog(@"%@", [NSThread currentThread]);
NSLog(@"4");
});
dispatch_async(queue, ^{
NSLog(@"%@", [NSThread currentThread]);
NSLog(@"5");
});
NSLog(@"222");
}
输出:
111
222
<NSThread: 0x6000019e2f40>{number = 3, name = (null)}
1
<NSThread: 0x6000019e2f40>{number = 3, name = (null)}
2
<NSThread: 0x6000019e2f40>{number = 3, name = (null)}
3
<NSThread: 0x6000019e2f40>{number = 3, name = (null)}
4
<NSThread: 0x6000019e2f40>{number = 3, name = (null)}
5
分析一下:
-
我们看到这里所有的任务都是使用dispatch_async来分配的,而dispatch_async不会阻塞调用它的线程,所以打印完“111”后就直接打印“222”了。
-
由于我们使用的是dispatch_async,所以GCD会开辟线程,同时由于是串行队列,所以GCD只开辟了一个线程来执行串行队列里的5个任务。
-
因为我们使用的是串行队列,所以队列里的任务在子线程中是按顺序执行的。
不过请注意:串行队列里的任务一定是按顺序执行的吗?
不是,这要看你创建了几个串行队列了,一个串行队列里的任务一定是按顺序执行的,但是多个串行队列之间是可以并行执行的。比如我们这里创建5个串行队列,一个队列里一个任务,那GCD就会开辟5个线程,这样5个串行队列里的任务就是并行执行。
但是这种做法是值得商榷的,因为大量的创建线程会造成很大的内存开销,所以在使用串行队列的时候,我们应该只创建绝对有必要的串行队列,而不能想创建多少就创建多少。
2、dispatch_async + 并行队列
我们把5个任务异步地分配到1个并行队列里:
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t queue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"111");
dispatch_async(queue, ^{
NSLog(@"%@", [NSThread currentThread]);
NSLog(@"1");
});
dispatch_async(queue, ^{
NSLog(@"%@", [NSThread currentThread]);
NSLog(@"2");
});
dispatch_async(queue, ^{
NSLog(@"%@", [NSThread currentThread]);
NSLog(@"3");
});
dispatch_async(queue, ^{
NSLog(@"%@", [NSThread currentThread]);
NSLog(@"4");
});
dispatch_async(queue, ^{
NSLog(@"%@", [NSThread currentThread]);
NSLog(@"5");
});
NSLog(@"222");
}
输出为:
111
222
<NSThread: 0x600002708180>{number = 6, name = (null)}
<NSThread: 0x600002708080>{number = 4, name = (null)}
<NSThread: 0x600002708140>{number = 5, name = (null)}
<NSThread: 0x600002707c80>{number = 3, name = (null)}
3
4
2
1
<NSThread: 0x600002708140>{number = 5, name = (null)}
5
分析一下:
-
我们看到这里所有的任务都是使用dispatch_async来分配的,而dispatch_async不会阻塞调用它的线程,所以打印完“111”后就直接打印“222”了。
-
由于我们使用的是dispatch_async,所以GCD会开辟线程,同时由于是并行队列,所以GCD开辟了多个线程来执行队列里的任务。
-
因为我们使用的是并行队列,所以队列里的任务在多个子线程中的执行是无序的。
3、dispatch_sync + 串行队列
我们把5个任务同步地分配到1个串行队列里:
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t queue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
NSLog(@"111");
dispatch_sync(queue, ^{
NSLog(@"%@", [NSThread currentThread]);
NSLog(@"1");
});
dispatch_sync(queue, ^{
NSLog(@"%@", [NSThread currentThread]);
NSLog(@"2");
});
dispatch_sync(queue, ^{
NSLog(@"%@", [NSThread currentThread]);
NSLog(@"3");
});
dispatch_sync(queue, ^{
NSLog(@"%@", [NSThread currentThread]);
NSLog(@"4");
});
dispatch_sync(queue, ^{
NSLog(@"%@", [NSThread currentThread]);
NSLog(@"5");
});
NSLog(@"222");
}
输出:
111
<NSThread: 0x6000032dc9c0>{number = 1, name = main}
1
<NSThread: 0x6000032dc9c0>{number = 1, name = main}
2
<NSThread: 0x6000032dc9c0>{number = 1, name = main}
3
<NSThread: 0x6000032dc9c0>{number = 1, name = main}
4
<NSThread: 0x6000032dc9c0>{number = 1, name = main}
5
222
分析一下:
-
我们看到这里所有的任务都是使用dispatch_sync来分配的,而dispatch_sync会阻塞调用它的线程,所以打印完“111”后,会执行它分配的第一个任务,执行完第一个任务后又会执行它分配的第二个任务,依次类推,直到dispatch_sync分配的所有任务都执行完了,线程阻塞结束,才打印“222”。
-
由于我们使用的是dispatch_sync,所以GCD不会开辟线程,而是把任务都拿到调用dispatch_sync函数的线程中去执行。
-
我们看到只要是使用dispatch_sync,是串行队列,任务是按顺序执行的。
4、dispatch_sync + 并行队列
我们把5个任务同步地分配到1个并行队列里:
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t queue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"111");
dispatch_sync(queue, ^{
NSLog(@"%@", [NSThread currentThread]);
NSLog(@"1");
});
dispatch_sync(queue, ^{
NSLog(@"%@", [NSThread currentThread]);
NSLog(@"2");
});
dispatch_sync(queue, ^{
NSLog(@"%@", [NSThread currentThread]);
NSLog(@"3");
});
dispatch_sync(queue, ^{
NSLog(@"%@", [NSThread currentThread]);
NSLog(@"4");
});
dispatch_sync(queue, ^{
NSLog(@"%@", [NSThread currentThread]);
NSLog(@"5");
});
NSLog(@"222");
}
输出:
111
<NSThread: 0x6000032dc9c0>{number = 1, name = main}
1
<NSThread: 0x6000032dc9c0>{number = 1, name = main}
2
<NSThread: 0x6000032dc9c0>{number = 1, name = main}
3
<NSThread: 0x6000032dc9c0>{number = 1, name = main}
4
<NSThread: 0x6000032dc9c0>{number = 1, name = main}
5
222
分析一下:
-
同3。
-
我们看到只要是使用dispatch_sync,这里虽然是并行队列,任务也是按顺序执行的。
四、死锁
其实下面两种情况,也只是一种情况了,因为主队列也是一个串行队列。所以我们可以总结一句话
在一个串行队列里,dispatch_sync一个任务到该串行队列,就会造成死锁
。
1、死锁情况一:在主线程中,dispatch_sync一个任务到主队列
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t queue = dispatch_get_main_queue();
NSLog(@"任务1");
dispatch_sync(queue, ^{
NSLog(@"任务2");
});
NSLog(@"任务3");
}
分析一下:
任务添加阶段:程序在启动后,系统会自动把主线程里要执行的任务都添加到主队列。那么此处,系统会把任务1和任务3先添加到主队列里,然后再把任务2追加到任务3后面。
任务执行阶段:任务在执行的时候,执行完任务1,dispatch_sync会阻塞主线程来执行任务2,但是我们上面说了任务2是被追加到任务3后面的,而队列又必须是先进先出,所以任务2想执行就得等任务3执行完,而这里任务3想要执行就必须得等任务2执行完,就造成了死锁。
2、死锁情况二:在一个串行队列里,dispatch_sync一个任务到该串行队列
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
NSLog(@"任务1");
dispatch_async(serialQueue, ^{
NSLog(@"任务2");
dispatch_sync(serialQueue, ^{
NSLog(@"任务3");
});
NSLog(@"任务4");
});
NSLog(@"任务5");
}
分析一下:
任务添加阶段:程序启动后,主队列内任务有1、5,自定义串行队列里有任务2、4、3。
任务执行阶段:任务在执行的时候,执行完任务1,dispatch_async不会阻塞主线程,所以会执行任务5,然后子线程中执行任务2,而dispatch_sync又会阻塞子线程来执行任务3,但是任务3是被追加到任务4后面的,而队列又必须是先进先出,所以任务3想执行就得等任务4执行完,而这里任务4想要执行就必须得等任务3执行完,就造成了死锁。
五、GCD的其它常用API
1、dispatch_once
dispatch_once用来保证一段代码在整个程序的生命周期内只执行一次,因此它是绝对线程安全的,创建单例的时候我们会用到它。
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 任务...
});
2、dispatch_after
dispatch_after用来延时多长时间后执行某个任务。
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 任务...
});
但是我们需要注意,这里其实并不是准确地在3s后执行某个任务,而是在延时指定的时间后把任务添加到指定的队列中,比如添加到主队列时,这个任务最快3s后执行,最慢(3+1/60)s后执行。
3、dispatch_group
当队列中所有的任务执行完毕后,想执行某个结束操作时,可以使用dispatch_group,这种情况我们经常会遇到。
这个队列当然可以是串行队列也可以是并行队列,只不过如果是串行队列时,我们可以把这个结束操作作为最后一个任务添加进去,不用dispatch_group也可以。例子如下:
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
NSLog(@"%@", [NSThread currentThread]);
NSLog(@"1");
});
dispatch_async(queue, ^{
NSLog(@"%@", [NSThread currentThread]);
NSLog(@"2");
});
dispatch_async(queue, ^{
NSLog(@"%@", [NSThread currentThread]);
NSLog(@"3");
});
dispatch_async(queue, ^{
NSLog(@"%@", [NSThread currentThread]);
NSLog(@"任务全部完成");
});
}
输出为:
<NSThread: 0x600003b7c340>{number = 3, name = (null)}
1
<NSThread: 0x600003b7c340>{number = 3, name = (null)}
2
<NSThread: 0x600003b7c340>{number = 3, name = (null)}
3
<NSThread: 0x600003b7c340>{number = 3, name = (null)}
任务全部完成
而并行队列就没办法了,如果我们想要在并行执行的多个任务执行结束后执行某个操作就必须用dispatch_group,因为并行执行的任务我们不知道它们的执行顺序,不知道到底哪一个任务才是最后执行完的那一个。例子如下:
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
NSLog(@"%@", [NSThread currentThread]);
NSLog(@"1");
});
dispatch_group_async(group, queue, ^{
NSLog(@"%@", [NSThread currentThread]);
NSLog(@"2");
});
dispatch_group_async(group, queue, ^{
NSLog(@"%@", [NSThread currentThread]);
NSLog(@"3");
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"任务全部完成");
});
}
输出为:
<NSThread: 0x6000005186c0>{number = 4, name = (null)}
<NSThread: 0x6000005175c0>{number = 3, name = (null)}
<NSThread: 0x600000517680>{number = 5, name = (null)}
2
1
3
任务全部完成
4、dispatch_barrier_async
dispatch_barrier_async,正如它的名字barrier一样,是个栅栏,它会把在它之前分配和在它之后分配到队列里的任务给分隔开,那么在它之前的分配的任务全部执行完毕之前,它之后分配的任务是绝对不会执行的。任务执行的顺序为:在它之前分配的任务全部执行完毕,执行dispatch_barrier_async的任务,然后再执行在它之后分配的任务,因此我们可以用它来完成高效的数据库访问,我们知道关于数据库的数据竞争问题,多个读操作竞争是不会发生数据问题的,而多个写操作竞争才会发生数据问题,因此我们通常的做法是把多个读操作放在并行队列里。
现在举个例子来看下,假设我们有个需求是读操作1、2、3可以随便地读,但是读操作4、5、6在读之前,必须执行一个写操作,而且还要保证读操作4、5、6读取的数据是新写进去的数据。那我们考虑到读操作数据竞争不会造成数据问题,所以会把这6个读操作放进一个并行队列里来提高数据读取的效率,那这个写操作该怎么办呢?怎么才能达到需求里的效果呢?用dispatch_barrier_async就可以了。如下:
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t queue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"读操作1");
});
dispatch_async(queue, ^{
NSLog(@"读操作2");
});
dispatch_async(queue, ^{
NSLog(@"读操作3");
});
dispatch_barrier_async(queue, ^{
NSLog(@"写操作");
});
dispatch_async(queue, ^{
NSLog(@"读操作4");
});
dispatch_async(queue, ^{
NSLog(@"读操作5");
});
dispatch_async(queue, ^{
NSLog(@"读操作6");
});
}
输出:
读操作2
读操作1
读操作3
写操作
读操作6
读操作4
读操作5
分析一下:
我们可以看到队列里本来有6个读任务,它们是并行执行的,但是当在中途插入了一个barrier任务之后,它就像栅栏一样把任务1、2、3和4、5、6给隔开了,在任务1、2、3执行完毕之前,任务4、5、6是绝对不可能执行的,只有等任务1、2、3完了,并且barrier任务执行了,任务4、5、6才会恢复它本来的状态并行执行。
5、dispatch_apply
dispatch_apply函数用来按指定的次数把指定的任务分配到队列中,它和dispatch_sync一样会阻塞当前线程。
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t queue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_apply(11, queue, ^(size_t index) {
NSLog(@"%ld", index);
});
NSLog(@"全部执行结束");
}
输出:
1
0
4
7
5
6
9
8
10
3
2
全部执行完毕