iOS多线程(上)——GCD详解(上)
GCD(Grand central Dispatch)是Apple开发的一个多核编程的较新的解决方法。它主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。下面我讲讲述关于GCD的点,通篇读完大约10-20分钟。
一、为什么要用GCD?
GCD是iOS线程的一种,也是被经常使用的一种方式。GCD也有很多的好处:
(1)GCD可用于多核的并行运算;
(2)GCD会自动利用更多的CPU内核;
(3)GCD会自动管理线程的生命周期;
(4)程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码
二、GCD任务和队列
先了解GCD中两个核心概念:任务和队列。
1.任务:就是执行操作的意思,换句话说就是你在线程中执行的那段代码。在GCD中是放在block中的。执行任务有两种方式:同步执行(sync)和异步执行(async)。两者的主要区别是:是否等待队列的任务执行结束,以及是否具备开启新线程的能力。
1).同步执行(sync):
同步添加到指定的队列中,在添加的任务执行结束之前,会一直等待,知道队列里面的任务完成之后再继续执行。
只能在当前线程中执行任务,不具备开启新线程的能力。
2).异步执行(async):
异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。
可以在新的线程中执行任务,具备开启新线程的能力。
注意: 异步执行(async) 虽然具有开启新线程的能力,但是并不一定开启新线程。这跟任务所指定的队列类型有关(下面会讲)。
2.队列:这里的队列指的是执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的线性表,采用 FIFO(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务。
在GCD中有两种队列:串行队列和并发队列。两者都符合FIFO(先进先出)的原则。两者的主要区别是:执行的顺序不同,以及开启线程数不同。
1)串行队列:
每次只有一个任务被执行。让任务一个接着一个地执行。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)
2)并发队列:
可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务)
注意:并发队列 的并发功能只有在异步(dispatch_async)函数下才有效
三、GCD的使用步骤
GCD的使用步骤其实比较简单,只有两步。
1.创建一个队列(串行队列或者并行队列)
2.将任务追加到任务的等待队列中,然后系统就会根据任务类型执行任务(同步执行或异步执行)
3.1队列的创建方法/获取方法
可以使用dispatch_queue_create来创建队列,需要传入两个参数,第一个参数表示队列的唯一标识符,用于DEBUG,可为空,Dispatch Queue的名称推荐使用应用程序ID这种逆序全程域名,第二个参数用来表示串行队列还是并发队列。DISPATCH_QUEUE_SERIAL 表示串行队列,DISPATCH_QUEUE_CONCURRENT表示并发队列。
下面是创建队列的例子:
// 串行队列的创建方法 dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_SERIAL); // 并发队列的创建方法 dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT);
对于串行队列,GCD提供了一种特殊的串行队列:主队列(Main Dispatch Queue)。
1)所有放在主队列的任务,都会放到主线程中执行。
2)可使用dispatch_get_main_queue()获得主队列。
// 主队列的获取方法 dispatch_queue_t queue = dispatch_get_main_queue();
对于并发队列,GCD默认提供了全局并发队列(Global Dispatch Queue)
可以使用dispatch_get_global_queue
来获取。需要传入两个参数。第一个参数是队列优先级,一般用DISPATCH_QUEUE_PRIORITY_DEFAULT。第二个参数暂时没用,可以用0即可。
// 全局并发队列的获取方法 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
3.2任务的创建方法
GCD提供了同步执行任务的创建方法dispatch_sync和异步执行任务创建方法dispatch_async。
// 同步执行任务创建方法 dispatch_sync(queue, ^{ // 这里放同步执行任务代码 }); // 异步执行任务创建方法 dispatch_async(queue, ^{ // 这里放异步执行任务代码 });
虽然使用GCD只需要两步,但是既然我们有两种队列(串行队列/并行队列),两种任务执行方式(同步执行/异步执行)那么我们就有了四种不同的组合方式。这四种不同的方式为:
1.同步执行 + 并发队列
2.异步执行 + 并发队列
3.同步执行 + 串行队列
4.异步执行 + 串行队列
实际上,刚才说了两种特殊队列:全局并发队列、主队列。全局并发队列可以作为普通并发队列来使用,但是主队列因为有点特殊,我们就又多了两种组合方式。我们就有了六种不同的方式了。
5.同步执行 + 主队列
6.异步执行 + 主队列
那么这几种不同组合方式各有什么区别呢,这里为了方便,先上结果,再来讲解。
四、GCD的基本使用
4.1 同步执行+并发队列
在当前线程中执行任务,不会开启新线程,执行完一个任务,再执行下一个任务。
(void)syncConcurrent { NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程 NSLog(@"syncConcurrent---begin"); dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT); dispatch_sync(queue, ^{ // 追加任务1 for (int i = 0; i < 2; ++i) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程 } }); dispatch_sync(queue, ^{ // 追加任务2 for (int i = 0; i < 2; ++i) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程 } }); dispatch_sync(queue, ^{ // 追加任务3 for (int i = 0; i < 2; ++i) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程 } }); NSLog(@"syncConcurrent---end");
打印输出结果:
输出结果: 2018-02-23 20:34:55.095932+0800 YSC-GCD-demo[19892:4996930] currentThread---{number = 1, name = main} 2018-04-23 20:34:55.096086+0800 YSC-GCD-demo[19892:4996930] syncConcurrent---begin 2018-04-23 20:34:57.097589+0800 YSC-GCD-demo[19892:4996930] 1---{number = 1, name = main} 2018-04-23 20:34:59.099100+0800 YSC-GCD-demo[19892:4996930] 1---{number = 1, name = main} 2018-04-23 20:35:01.099843+0800 YSC-GCD-demo[19892:4996930] 2---{number = 1, name = main} 2018-04-23 20:35:03.101171+0800 YSC-GCD-demo[19892:4996930] 2---{number = 1, name = main} 2018-04-23 20:35:05.101750+0800 YSC-GCD-demo[19892:4996930] 3---{number = 1, name = main} 2018-04-23 20:35:07.102414+0800 YSC-GCD-demo[19892:4996930] 3---{number = 1, name = main} 2018-04-23 20:35:07.102575+0800 YSC-GCD-demo[19892:4996930] syncConcurrent---end
从结果可以看到:
(1)所有任务都是在当前线程(主线程)中执行的,没有开启新的线程(同步执行不具备开启新线程的能力)。
(2)所有任务都在打印的syncConcurrent---begin和syncConcurrent---end之间执行的(同步任务需要等待队列的任务执行结束)。
(3)任务按顺序执行的。按顺序执行的原因:虽然并发队列可以开启多个线程,并且同时执行多个任务。但是因为本身不能创建新线程,只有当前线程这一个线程(同步任务不具备开启新线程的能力),所以也就不存在并发。而且当前线程只有等待当前队列中正在执行的任务执行完毕之后,才能继续接着执行下面的操作(同步任务需要等待队列的任务执行结束)。所以任务只能一个接一个按顺序执行,不能同时被执行。
4.2 异步执行+并发队列
可以开启多个线程,任务交替(同时)执行
- (void)asyncConcurrent { NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程 NSLog(@"asyncConcurrent---begin"); dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT); dispatch_async(queue, ^{ // 追加任务1 for (int i = 0; i < 2; ++i) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程 } }); dispatch_async(queue, ^{ // 追加任务2 for (int i = 0; i < 2; ++i) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程 } }); dispatch_async(queue, ^{ // 追加任务3 for (int i = 0; i < 2; ++i) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程 } }); NSLog(@"asyncConcurrent---end"); }
结果如下:
输出结果: 2018-02-23 20:36:41.769269+0800 YSC-GCD-demo[19929:5005237] currentThread---{number = 1, name = main} 2018-04-23 20:36:41.769496+0800 YSC-GCD-demo[19929:5005237] asyncConcurrent---begin 2018-04-23 20:36:41.769725+0800 YSC-GCD-demo[19929:5005237] asyncConcurrent---end 2018-04-23 20:36:43.774442+0800 YSC-GCD-demo[19929:5005566] 2---{number = 5, name = (null)} 2018-04-23 20:36:43.774440+0800 YSC-GCD-demo[19929:5005567] 3---{number = 4, name = (null)} 2018-04-23 20:36:43.774440+0800 YSC-GCD-demo[19929:5005565] 1---{number = 3, name = (null)} 2018-04-23 20:36:45.779286+0800 YSC-GCD-demo[19929:5005567] 3---{number = 4, name = (null)} 2018-04-23 20:36:45.779302+0800 YSC-GCD-demo[19929:5005565] 1---{number = 3, name = (null)} 2018-04-23 20:36:45.779286+0800 YSC-GCD-demo[19929:5005566] 2---{number = 5, name = (null)}
在异步执行+并发队列可以看出:
除了当前线程(主线程),系统又开启了3个线程,并且任务是交替/同时执行的。(异步执行具备开启新线程的能力。且并发队列可开启多个线程,同时执行多个任务)。所有任务是在打印的syncConcurrent---begin和syncConcurrent---end之后才执行的。说明当前线程没有等待,而是直接开启了新线程,在新线程中执行任务(异步执行不做等待,可以继续执行任务)。
4.3 同步执行 + 串行队列
不会开启新线程,在当前线程执行任务。任务是串行的,执行完一个任务,再执行下一个任务。
- (void)syncSerial { NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程 NSLog(@"syncSerial---begin"); dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_SERIAL); dispatch_sync(queue, ^{ // 追加任务1 for (int i = 0; i < 2; ++i) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程 } }); dispatch_sync(queue, ^{ // 追加任务2 for (int i = 0; i < 2; ++i) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程 } }); dispatch_sync(queue, ^{ // 追加任务3 for (int i = 0; i < 2; ++i) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程 } }); NSLog(@"syncSerial---end"); }
输出结果:
2018-02-23 20:39:37.876811+0800 YSC-GCD-demo[19975:5017162] currentThread---{number = 1, name = main} 2018-04-23 20:39:37.876998+0800 YSC-GCD-demo[19975:5017162] syncSerial---begin 2018-04-23 20:39:39.878316+0800 YSC-GCD-demo[19975:5017162] 1---{number = 1, name = main} 2018-04-23 20:39:41.879829+0800 YSC-GCD-demo[19975:5017162] 1---{number = 1, name = main} 2018-04-23 20:39:43.880660+0800 YSC-GCD-demo[19975:5017162] 2---{number = 1, name = main} 2018-04-23 20:39:45.881265+0800 YSC-GCD-demo[19975:5017162] 2---{number = 1, name = main} 2018-04-23 20:39:47.882257+0800 YSC-GCD-demo[19975:5017162] 3---{number = 1, name = main} 2018-04-23 20:39:49.883008+0800 YSC-GCD-demo[19975:5017162] 3---{number = 1, name = main} 2018-04-23 20:39:49.883253+0800 YSC-GCD-demo[19975:5017162] syncSerial---end
在同步执行+串行队列可以看出
(1)所有任务都是在当前线程(主线程)中执行的,并没有开启新的线程(同步执行不具备开启新线程的能力)。
(2)所有任务都在打印的syncConcurrent---begin和syncConcurrent---end之间执行(同步任务需要等待队列的任务执行结束)。
(3)任务是按顺序执行的(串行队列每次只有一个任务被执行,任务一个接一个按顺序执行)。
4.4 异步执行 + 串行队列
会开启新线程,但是因为任务是串行的,执行完一个任务,再执行下一个任务
- (void)asyncSerial { NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程 NSLog(@"asyncSerial---begin"); dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_SERIAL); dispatch_async(queue, ^{ // 追加任务1 for (int i = 0; i < 2; ++i) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程 } }); dispatch_async(queue, ^{ // 追加任务2 for (int i = 0; i < 2; ++i) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程 } }); dispatch_async(queue, ^{ // 追加任务3 for (int i = 0; i < 2; ++i) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程 } }); NSLog(@"asyncSerial---end"); }
执行结果如下:
2018-04-23 20:41:17.029999+0800 YSC-GCD-demo[20008:5024757] currentThread---{number = 1, name = main} 2018-04-23 20:41:17.030212+0800 YSC-GCD-demo[20008:5024757] asyncSerial---begin 2018-04-23 20:41:17.030364+0800 YSC-GCD-demo[20008:5024757] asyncSerial---end 2018-04-23 20:41:19.035379+0800 YSC-GCD-demo[20008:5024950] 1---{number = 3, name = (null)} 2018-04-23 20:41:21.037140+0800 YSC-GCD-demo[20008:5024950] 1---{number = 3, name = (null)} 2018-04-23 20:41:23.042220+0800 YSC-GCD-demo[20008:5024950] 2---{number = 3, name = (null)} 2018-04-23 20:41:25.042971+0800 YSC-GCD-demo[20008:5024950] 2---{number = 3, name = (null)} 2018-04-23 20:41:27.047690+0800 YSC-GCD-demo[20008:5024950] 3---{number = 3, name = (null)} 2018-04-23 20:41:29.052327+0800 YSC-GCD-demo[20008:5024950] 3---{number = 3, name = (null)}
从执行结果看:
(1)开启了一条新线程(异步执行具备开启新线程的能力,串行队列只开启一个线程)。
(2)所有任务是在打印的syncConcurrent---begin和syncConcurrent---end之后才开始执行的(异步执行不会做任何等待,可以继续执行任务)。
(3)任务是按顺序执行的(串行队列每次只有一个任务被执行,任务一个接一个按顺序执行)。
下边讲讲刚才我们提到过的特殊队列:主队列。
主队列:GCD自带的一种特殊的串行队列
所有放在主队列中的任务,都会放到主线程中执行
可使用dispatch_get_main_queue()获得主队列
我们再来看看主队列的两种组合方式。
4.5 同步执行 + 主队列
同步执行 + 主队列在不同线程中调用结果也是不一样,在主线程中调用会出现死锁,而在其他线程中则不会。
/** * 同步执行 + 主队列 * 特点(主线程调用):互等卡主不执行。 * 特点(其他线程调用):不会开启新线程,执行完一个任务,再执行下一个任务。 */ - (void)syncMain { NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程 NSLog(@"syncMain---begin"); dispatch_queue_t queue = dispatch_get_main_queue(); dispatch_sync(queue, ^{ // 追加任务1 for (int i = 0; i < 2; ++i) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程 } }); dispatch_sync(queue, ^{ // 追加任务2 for (int i = 0; i < 2; ++i) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程 } }); dispatch_sync(queue, ^{ // 追加任务3 for (int i = 0; i < 2; ++i) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程 } }); NSLog(@"syncMain---end"); }
输出结果:
2018-02-23 20:42:36.842892+0800 YSC-GCD-demo[20041:5030982] currentThread---{number = 1, name = main} 2018-02-23 20:42:36.843050+0800 YSC-GCD-demo[20041:5030982] syncMain---begin (lldb)
在同步执行 + 主队列可以惊奇的发现:
在主线程中使用同步执行 + 主队列,追加到主线程的任务1、任务2、任务3都不再执行了,而且syncMain---end也没有打印,在XCode 9上还会报崩溃。这是为什么呢?
这是因为我们在主线程中执行syncMain方法,相当于把syncMain任务放到了主线程的队列中。而同步执行会等待当前队列中的任务执行完毕,才会接着执行。那么当我们把任务1追加到主队列中,任务1就在等待主线程处理完syncMain任务。而syncMain任务需要等待任务1执行完毕,才能接着执行。
那么,现在的情况就是syncMain任务和任务1都在等对方执行完毕。这样大家互相等待,所以就卡住了,所以我们的任务执行不了,而且syncMain---end也没有打印。
要是如果不在主线程中调用,而在其他线程中调用会如何呢
4.5.2 在其他线程中调用同步执行 + 主队列
// 使用 NSThread 的 detachNewThreadSelector 方法会创建线程,并自动启动线程执行 selector 任务 [NSThread detachNewThreadSelector:@selector(syncMain) toTarget:self withObject:nil];
输出结果如下:
2018-02-23 20:44:19.377321+0800 YSC-GCD-demo[20083:5040347] currentThread---{number = 3, name = (null)} 2018-04-23 20:44:19.377494+0800 YSC-GCD-demo[20083:5040347] syncMain---begin 2018-04-23 20:44:21.384716+0800 YSC-GCD-demo[20083:5040132] 1---{number = 1, name = main} 2018-04-23 20:44:23.386091+0800 YSC-GCD-demo[20083:5040132] 1---{number = 1, name = main} 2018-04-23 20:44:25.387687+0800 YSC-GCD-demo[20083:5040132] 2---{number = 1, name = main} 2018-4-23 20:44:27.388648+0800 YSC-GCD-demo[20083:5040132] 2---{number = 1, name = main} 2018-04-23 20:44:29.390459+0800 YSC-GCD-demo[20083:5040132] 3---{number = 1, name = main} 2018-04-23 20:44:31.391965+0800 YSC-GCD-demo[20083:5040132] 3---{number = 1, name = main} 2018-04-23 20:44:31.392513+0800 YSC-GCD-demo[20083:5040347] syncMain---end
在其他线程中使用同步执行 + 主队列可看到:
(1)所有任务都是在主线程(非当前线程)中执行的,没有开启新的线程(所有放在主队列中的任务,都会放到主线程中执行)。
(2)所有任务都在打印的syncConcurrent---begin和syncConcurrent---end之间执行(同步任务需要等待队列的任务执行结束)。
(3)任务是按顺序执行的(主队列是串行队列,每次只有一个任务被执行,任务一个接一个按顺序执行)。
为什么现在就不会卡住了呢?
因为syncMain 任务放到了其他线程里,而任务1、任务2、任务3都在追加到主队列中,这三个任务都会在主线程中执行。syncMain 任务在其他线程中执行到追加任务1到主队列中,因为主队列现在没有正在执行的任务,所以,会直接执行主队列的任务1,等任务1执行完毕,再接着执行任务2、任务3。所以这里不会卡住线程。
4.6 异步执行 + 主队列
只在主线程中执行任务,执行完一个任务,再执行下一个任务。
- (void)asyncMain { NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程 NSLog(@"asyncMain---begin"); dispatch_queue_t queue = dispatch_get_main_queue(); dispatch_async(queue, ^{ // 追加任务1 for (int i = 0; i < 2; ++i) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程 } }); dispatch_async(queue, ^{ // 追加任务2 for (int i = 0; i < 2; ++i) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程 } }); dispatch_async(queue, ^{ // 追加任务3 for (int i = 0; i < 2; ++i) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程 } }); NSLog(@"asyncMain---end"); }
执行结果如下:
2018-02-23 20:45:49.981505+0800 YSC-GCD-demo[20111:5046708] currentThread---{number = 1, name = main} 2018-04-23 20:45:49.981935+0800 YSC-GCD-demo[20111:5046708] asyncMain---begin 2018-04-23 20:45:49.982352+0800 YSC-GCD-demo[20111:5046708] asyncMain---end 2018-04-23 20:45:51.991096+0800 YSC-GCD-demo[20111:5046708] 1---{number = 1, name = main} 2018-04-23 20:45:53.991959+0800 YSC-GCD-demo[20111:5046708] 1---{number = 1, name = main} 2018-04-23 20:45:55.992937+0800 YSC-GCD-demo[20111:5046708] 2---{number = 1, name = main} 2018-04-23 20:45:57.993649+0800 YSC-GCD-demo[20111:5046708] 2---{number = 1, name = main} 2018-04-23 20:45:59.994928+0800 YSC-GCD-demo[20111:5046708] 3---{number = 1, name = main} 2018-04-23 20:46:01.995589+0800 YSC-GCD-demo[20111:5046708] 3---{number = 1, name = main}
在异步执行 + 主队列可以看到:
(1)所有任务都是在当前线程(主线程)中执行的,并没有开启新的线程(虽然异步执行具备开启线程的能力,但因为是主队列,所以所有任务都在主线程中)。
(2)所有任务是在打印的syncConcurrent---begin和syncConcurrent---end之后才开始执行的(异步执行不会做任何等待,可以继续执行任务)。
(3)任务是按顺序执行的(因为主队列是串行队列,每次只有一个任务被执行,任务一个接一个按顺序执行)。
对于上面是GCD的基本介绍,下一篇将讲述GCD的使用,谢谢观看!!!