一、GCD简介
GCD全称是Grand Central Dispatch
,是Apple开发中用于实现多线程编程的一种解决方案。GCD是纯C
语言,提供了很多强大的函数。
GCD的优势如下:
- GCD 可用于多核的并行运算;
- GCD 会自动利用更多的CPU内核(比如双核、四核);
- GCD 会自动管理线程的生命周期(创建线程、调度任务、销毁线程);
- 代码简洁,不需要编写任何线程管理的代码。
二、GCD的任务和队列
GCD中有两个核心概念:『任务』和『队列』。
1.任务
任务即执行操作,简单来说是在线程上执行的那段代码,在GCD中被封装在block
中。任务的执行方式有两种:『同步执行』,『异步执行』。
同步执行(
sync
)
- 不会开启新线程,只在当前线程中串行执行任务,需要等待。
- 对于代码执行来说,必须等待当前语句执⾏完毕,才会执⾏下⼀条语句。
异步执行(
async
)
- 会开启新线程,并发执行任务,不需等待。
- 对于代码执行来说,不⽤等待当前语句执⾏完毕,就可以执⾏下⼀条语句。
2.队列(Dispatch Queue)
这里的队列指执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的线性表,采用FIFO(先进先出)
的原则,即最早被添加的任务会先被调度执行。每执行一个任务,就从队列中释放一个任务。队列的结构可参考下图:
GCD中的队列也分为两种:『串行队列』,『并发队列』。
-
串行队列(
Serial Dispatch Queue
):只开启一个线程,同一时间只执行一个任务,一个任务执行完毕后,再执行下一个任务。 -
并发队列(
Concurrent Dispatch Queue
):可以开启多个线程,同一时间可以并发执行多个任务,不用等待。
注意:并发队列中的任务只有在异步执行下才会开启多个线程,同步执行下只会在当前线程中处理任务。
两者的任务执行情况如下图所示:
三、GCD的使用步骤
GCD使用步骤教简单,只有两步:
1. 创建一个队列(串行队列或者并行队列)。
2. 创建任务,将任务添加到队列(创建时需指定是同步执行还是异步执行)。
1.队列的创建和获取
-
创建串行队列和并发队列
通过
dispatch_queue_create
函数可以创建串行队列和并发队列,函数需要传入两个参数:- 参数1:为队列的标识符,可以为空;
- 参数2:为队列的类型,
DISPATCH_QUEUE_SERIAL
表示串行队列,DISPATCH_QUEUE_CONCURRENT
表示并发队列。其中DISPATCH_QUEUE_SERIAL
本质是NULL
。
//创建一个串行队列,参数2也可以直接传NULL dispatch_queue_t sQueue = dispatch_queue_create("lab1", DISPATCH_QUEUE_SERIAL); //创建一个并发队列 dispatch_queue_t cQueue = dispatch_queue_create("lab2", DISPATCH_QUEUE_CONCURRENT); #define DISPATCH_QUEUE_SERIAL NULL
-
获取主队列(Main Dispatch Queue)
主队列是一种特殊的串行队列,程序启动时由系统默认提供,所有任务均默认添加在主队列。主队列中所有的任务,无论是同步执行还是异步执行,都只会在主线程中执行。通过
dispatch_get_main_queue()
函数来获取主队列:dispatch_queue_t mainQueue = dispatch_get_main_queue();
-
获取全局并发队列(Global Dispatch Queue)
在使⽤多线程开发时,如果对队列没有特殊需求,在执⾏异步任务时,可以直接使⽤全局并发队列。通过
dispatch_get_global_queue
函数来获取全局并发队列,需要传入两个参数:- 参数1:表示队列优先级,一般传入
DISPATCH_QUEUE_PRIORITY_DEFAULT
。 - 参数2:暂时没用,传
0
即可。
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- 参数1:表示队列优先级,一般传入
2.任务的创建和添加
GCD 提供了同步执行任务的创建方法dispatch_sync
和异步执行任务创建方法dispatch_async
。
//同步执行任务创建方法
dispatch_sync(queue, ^{
//这里放同步执行任务代码
});
//异步执行任务创建方法
dispatch_async(queue, ^{
//这里放异步执行任务代码
});
四、对串行队列的分析
串行队列里的任务,会遵循FIFO规则
依次调度执行,由于任务的执行方式分为同步执行和异步执行,所以串行队列中的任务存在以下的情况:
- 全部都是同步执行的任务
- 全部都是异步执行的任务
- 既有同步执行又有异步执行的任务
- 线程死锁
接下来通过具体示例来分析这几种组合情况。
1. 全部都是同步执行的任务
如下面代码所示,往一个串行队列中添加三个同步执行任务,然后打印执行结果。
//串行队列+同步执行任务
-(void)testSerialAndSync{
NSLog(@" ==== method thread = %@", [NSThread currentThread]);
NSLog(@" ==== begin");
dispatch_queue_t queue = dispatch_queue_create("", DISPATCH_QUEUE_SERIAL);
//添加同步执行任务1
dispatch_sync(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@" ==== task1 thread = %@", [NSThread currentThread]);
});
//添加同步执行任务2
dispatch_sync(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@" ==== task2 thread = %@", [NSThread currentThread]);
});
//添加同步执行任务3
dispatch_sync(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@" ==== task3 thread = %@", [NSThread currentThread]);
});
NSLog(@" ==== end");
}
//打印结果
2020-11-15 01:16:07.499128+0800 GCDDemo[17257:70356957] ==== method thread = <NSThread: 0x600001704100>{number = 1, name = main}
2020-11-15 01:16:07.499199+0800 GCDDemo[17257:70356957] ==== begin
2020-11-15 01:16:09.500480+0800 GCDDemo[17257:70356957] ==== task1 thread = <NSThread: 0x600001704100>{number = 1, name = main}
2020-11-15 01:16:11.501599+0800 GCDDemo[17257:70356957] ==== task2 thread = <NSThread: 0x600001704100>{number = 1, name = main}
2020-11-15 01:16:13.502912+0800 GCDDemo[17257:70356957] ==== task3 thread = <NSThread: 0x600001704100>{number = 1, name = main}
2020-11-15 01:16:13.503060+0800 GCDDemo[17257:70356957] ==== end
从打印结果可知,三个任务均在当前线程(主线程)里按顺序依次执行。因此,当一个串行队列里都是同步执行的任务,不会开辟新线程,所有任务均在当前线程同步执行。
2. 全部都是异步执行的任务
如下面代码所示,往一个串行队列中添加三个异步执行任务,然后打印执行结果。
//串行队列+异步执行任务
-(void)testSerialAndAsync{
NSLog(@" ==== method thread = %@", [NSThread currentThread]);
NSLog(@" ==== begin");
dispatch_queue_t queue = dispatch_queue_create("", DISPATCH_QUEUE_SERIAL);
//添加异步执行任务1
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:3];
NSLog(@" ==== task1 thread = %@", [NSThread currentThread]);
});
//添加异步执行任务2
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@" ==== task2 thread = %@", [NSThread currentThread]);
});
//添加异步执行任务3
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:1];
NSLog(@" ==== task3 thread = %@", [NSThread currentThread]);
});
NSLog(@" ==== end");
}
//打印结果
2020-11-16 10:12:25.065872+0800 GCDDemo[41263:73331602] ==== method thread = <NSThread: 0x60000170c100>{number = 1, name = main}
2020-11-16 10:12:25.065953+0800 GCDDemo[41263:73331602] ==== begin
2020-11-16 10:12:25.066017+0800 GCDDemo[41263:73331602] ==== end
2020-11-16 10:12:28.071048+0800 GCDDemo[41263:73331984] ==== task1 thread = <NSThread: 0x60000175c2c0>{number = 2, name = (null)}
2020-11-16 10:12:30.074008+0800 GCDDemo[41263:73331984] ==== task2 thread = <NSThread: 0x60000175c2c0>{number = 2, name = (null)}
2020-11-16 10:12:31.076458+0800 GCDDemo[41263:73331984] ==== task3 thread = <NSThread: 0x60000175c2c0>{number = 2, name = (null)}
从打印结果可知,这个过程中只新开启了一条子线程,三个任务在子线程中按序依次执行。因此,当一个串行队列里全是异步执行的任务,只会开启一个新线程,所有任务在这个线程里按顺序同步执行。
3. 既有同步执行又有异步执行的任务
如下面代码所示,往一个串行队列中穿插添加两个同步执行的任务和两个异步执行的任务。
//串行队列+两种方式执行的任务
-(void)testSerial{
NSLog(@" ==== method thread = %@", [NSThread currentThread]);
NSLog(@" ==== begin");
dispatch_queue_t queue = dispatch_queue_create("", DISPATCH_QUEUE_SERIAL);
//添加异步执行任务1
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:4];
NSLog(@" ==== task1 thread = %@", [NSThread currentThread]);
});
//添加同步执行任务2
dispatch_sync(queue, ^{
[NSThread sleepForTimeInterval:3];
NSLog(@" ==== task2 thread = %@", [NSThread currentThread]);
});
//添加异步执行任务3
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@" ==== task3 thread = %@", [NSThread currentThread]);
});
//添加同步执行任务4
dispatch_sync(queue, ^{
[NSThread sleepForTimeInterval:1];
NSLog(@" ==== task4 thread = %@", [NSThread currentThread]);
});
NSLog(@" ==== end");
}
//打印结果
2020-11-16 14:25:03.502639+0800 GCDDemo[56034:75334852] ==== method thread = <NSThread: 0x600001704140>{number = 1, name = main}
2020-11-16 14:25:03.502730+0800 GCDDemo[56034:75334852] ==== begin
2020-11-16 14:25:07.503842+0800 GCDDemo[56034:75335420] ==== task1 thread = <NSThread: 0x600001749a80>{number = 2, name = (null)}
2020-11-16 14:25:10.505354+0800 GCDDemo[56034:75334852] ==== task2 thread = <NSThread: 0x600001704140>{number = 1, name = main}
2020-11-16 14:25:12.506879+0800 GCDDemo[56034:75335420] ==== task3 thread = <NSThread: 0x600001749a80>{number = 2, name = (null)}
2020-11-16 14:25:13.508375+0800 GCDDemo[56034:75334852] ==== task4 thread = <NSThread: 0x600001704140>{number = 1, name = main}
2020-11-16 14:25:13.508554+0800 GCDDemo[56034:75334852] ==== end
从打印结果可知,这个过程中新开启了一条子线程,两个异步任务在子线程执行,两个同步任务在当前线程(主线程)中执行,且这4个任务是按顺序依次执行。因此,当一个串行队列中既有同步执行任务,还有异步执行任务时,会新开启一个子线程,所有异步任务均在子线程上执行,所有同步任务均在当前线程中执行,虽然是在两个线程上执行,但所有任务依旧按序同步执行。
4. 线程死锁
当往一个串行队列中添加同步执行任务时,若使用不当,可能会导致线程死锁,这里对普通串行队列和主队列来分别进行分析。
- 普通串行队列死锁
-(void)testSerialAndSyncLock{
NSLog(@" ==== method thread = %@", [NSThread currentThread]);
NSLog(@" ==== begin");
dispatch_queue_t queue = dispatch_queue_create("", DISPATCH_QUEUE_SERIAL);
//添加异步执行任务1
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@" ==== task1 thread = %@", [NSThread currentThread]);
//添加同步执行任务2
dispatch_sync(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@" ==== task2 thread = %@", [NSThread currentThread]);
});
});
NSLog(@" ==== end");
}
//打印结果
2020-11-16 14:48:47.797169+0800 GCDDemo[57469:75514015] ==== method thread = <NSThread: 0x600001708640>{number = 1, name = main}
2020-11-16 14:48:47.797242+0800 GCDDemo[57469:75514015] ==== begin
2020-11-16 14:48:47.797304+0800 GCDDemo[57469:75514015] ==== end
2020-11-16 14:48:49.802076+0800 GCDDemo[57469:75514581] ==== task1 thread = <NSThread: 0x600001757dc0>{number = 2, name = (null)}
//报错
Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
dispatch_sync_f_slow
在这个例子中,先往串行队列queue中添加一个异步执行任务task1,然后在task1里又往队列中添加同步执行任务task2。从前面的分析可知,task1会在子线程中执行,task2会在主线程中执行,但从打印结果可知,子线程死锁了。
为什么会这样呢?从代码逻辑可知,串行队列queue里的任务情况如下图所示:
由于是串行队列,所以需要先执行完task1,才能执行task2。上文已说明,任务其实是写在block里的代码,所以task1里具体包含sleepForTimeInterval
、NSLog
、dispatch_sync
这三个操作,而执行dispatch_sync
操作,必须要先执行其block里的任务(也就是task2),所以这里完成task1的执行需要先等待task2执行完,而task2的执行又必须等待task1的完成,这样就造成了两个任务互相等待的局面,导致串行队列阻塞,使得正在执行任务的子线程死锁。
- 主队列死锁
-(void)testMainQueueAndSyncLock{
NSLog(@" ==== method thread = %@", [NSThread currentThread]);
NSLog(@" ==== begin");
dispatch_queue_t queue = dispatch_get_main_queue();
//添加同步执行任务1
dispatch_sync(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@" ==== task1 thread = %@", [NSThread currentThread]);
});
NSLog(@" ==== end");
}
//执行结果
2020-11-16 15:10:25.390210+0800 GCDDemo[58693:75674415] ==== method thread = <NSThread: 0x600001709040>{number = 1, name = main}
2020-11-16 15:10:25.390282+0800 GCDDemo[58693:75674415] ==== begin
//报错
Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
dispatch_sync_f_slow
这里主线程死锁的原因和上面一样,虽然说task1里没有添加其他同步执行任务,但是外层方法testMainQueueAndSyncLock
本身就是添加在主队列里的任务,方法里包含了dispatch_sync
操作,所以需要先执行block里的任务(即task1)。而这个dispatch_sync
操作是将task1添加到主队列中,所以又需要先等外层方法testMainQueueAndSyncLock
执行完才能执行task1,这样就造成外层方法和task1互相等待,阻塞了主队列,导致主线程死锁。
- 主队列+同步执行不一定会死锁
这里要特别说明一下,之前有看过其他文章,说主队列+同步执行
一定会造成死锁,其实是不准确的,只是因为“执行主队列+同步执行
的任务”本身也处在主队列中同步执行,所以会导致主线程死锁。来看下面的例子:
-(void)testMainQueueAndSyncLock{
NSLog(@" ==== method thread = %@", [NSThread currentThread]);
NSLog(@" ==== begin");
dispatch_queue_t queue = dispatch_queue_create("lab", DISPATCH_QUEUE_SERIAL);
//添加异步执行任务1
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@" ==== task1 thread = %@", [NSThread currentThread]);
//添加同步执行任务2到主队列中
dispatch_sync(dispatch_get_main_queue(), ^{
[NSThread sleepForTimeInterval:2];
NSLog(@" ==== task2 thread = %@", [NSThread currentThread]);
});
});
//这里延迟8s
[NSThread sleepForTimeInterval:8];
NSLog(@" ==== end");
}
//打印结果
2020-11-16 15:22:38.153895+0800 GCDDemo[59402:75766119] ==== method thread = <NSThread: 0x600001708280>{number = 1, name = main}
2020-11-16 15:22:38.153978+0800 GCDDemo[59402:75766119] ==== begin
2020-11-16 15:22:40.156534+0800 GCDDemo[59402:75766421] ==== task1 thread = <NSThread: 0x60000174b440>{number = 2, name = (null)}
2020-11-16 15:22:46.154269+0800 GCDDemo[59402:75766119] ==== end
2020-11-16 15:22:48.247564+0800 GCDDemo[59402:75766119] ==== task2 thread = <NSThread: 0x600001708280>{number = 1, name = main}
这里先往一个串行队列queue中添加一个异步执行任务task1,再在tsak1里往主队列中添加同步执行任务task2,从执行结果可知,并未死锁。通过代码逻辑可知,在这个过程中存在两个队列:主队列和串行队列queue。串行队列queue中只有一个异步执行的任务task1,不会造成死锁,所以主要看主队列中的情况。
主队列里存在两个同步执行任务,会先执行任务外层方法testMainQueueAndSyncLock
,再执行task2。外层方法里包含了NSLog
、sleepForTimeInterval
、dispatch_async
这几个操作,而执行dispatch_async
的操作不需要先将其block里的任务执行完,所以可以直接将外层方法执行完,然后再执行另一个任务task2,这样两个任务不会互相等待,也就不会导致主线程死锁。
5. 串行队列分析后的结论
根据上面的分析可知,串行队列在各场景下的使用情况如下:
- 当一个串行队列里都是同步执行的任务,不会开辟新线程,所有任务均在当前线程同步执行。
- 当一个串行队列里全是异步执行的任务,只会开启一个新线程,所有任务在这个线程里按顺序同步执行。
- 当一个串行队列中既有同步执行任务,还有异步执行任务时,会新开启一个子线程,所有异步任务均在子线程上执行,所有同步任务均在当前线程中执行,虽然是在两个线程上执行,但所有任务依旧按序同步执行。
- 若在某个串行队列中使用
当前串行队列+同步执行任务
,则会阻塞当前队列,导致当前线程死锁。
五、对并发队列的分析
并发队列也遵循FIFO原则
,和串行队列一样,也会存在以下几种情况:
- 全部都是同步执行的任务
- 全部都是同步执行的任务
- 既有同步执行又有异步执行的任务
接下来也通过具体示例来分析这几种组合情况。
1. 全部都是同步执行的任务
如下面代码所示,往一个并发队列中添加三个同步执行任务,然后打印执行结果。
//并发队列+同步执行任务
-(void)testConcurrentAndSync{
NSLog(@" ==== method thread = %@", [NSThread currentThread]);
NSLog(@" ==== begin");
dispatch_queue_t queue = dispatch_queue_create("", DISPATCH_QUEUE_CONCURRENT);
//添加同步执行任务1
dispatch_sync(queue, ^{
[NSThread sleepForTimeInterval:3];
NSLog(@" ==== task1 thread = %@", [NSThread currentThread]);
});
//添加同步执行任务2
dispatch_sync(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@" ==== task2 thread = %@", [NSThread currentThread]);
});
//添加同步执行任务3
dispatch_sync(queue, ^{
[NSThread sleepForTimeInterval:1];
NSLog(@" ==== task3 thread = %@", [NSThread currentThread]);
});
NSLog(@" ==== end");
}
2020-11-16 16:03:40.559502+0800 GCDDemo[61734:76074945] ==== method thread = <NSThread: 0x60000170ce40>{number = 1, name = main}
2020-11-16 16:03:40.559576+0800 GCDDemo[61734:76074945] ==== begin
2020-11-16 16:03:43.559757+0800 GCDDemo[61734:76074945] ==== task1 thread = <NSThread: 0x60000170ce40>{number = 1, name = main}
2020-11-16 16:03:45.560417+0800 GCDDemo[61734:76074945] ==== task2 thread = <NSThread: 0x60000170ce40>{number = 1, name = main}
2020-11-16 16:03:46.560727+0800 GCDDemo[61734:76074945] ==== task3 thread = <NSThread: 0x60000170ce40>{number = 1, name = main}
2020-11-16 16:03:46.560801+0800 GCDDemo[61734:76074945] ==== end
从打印结果可知,这三个任务均在主线程中按序执行。因此,当一个并发队列里都是同步执行的任务,不会开辟新线程,所有任务均在当前线程同步执行。
2. 全部都是异步执行的任务
如下面代码所示,往一个并发队列中添加三个异步执行任务,然后打印执行结果。
//并发队列+异步执行任务
-(void)testConcurrentAndAsync{
NSLog(@" ==== method thread = %@", [NSThread currentThread]);
NSLog(@" ==== begin");
dispatch_queue_t queue = dispatch_queue_create("", DISPATCH_QUEUE_CONCURRENT);
//添加同步执行任务1
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:3];
NSLog(@" ==== task1 thread = %@", [NSThread currentThread]);
});
//添加同步执行任务2
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@" ==== task2 thread = %@", [NSThread currentThread]);
});
//添加同步执行任务3
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:1];
NSLog(@" ==== task3 thread = %@", [NSThread currentThread]);
});
NSLog(@" ==== end");
}
//打印结果
2020-11-16 16:08:30.368449+0800 GCDDemo[62020:76112767] ==== method thread = <NSThread: 0x600001708600>{number = 1, name = main}
2020-11-16 16:08:30.368531+0800 GCDDemo[62020:76112767] ==== begin
2020-11-16 16:08:30.368594+0800 GCDDemo[62020:76112767] ==== end
2020-11-16 16:08:31.369037+0800 GCDDemo[62020:76113117] ==== task3 thread = <NSThread: 0x60000176a980>{number = 2, name = (null)}
2020-11-16 16:08:32.372892+0800 GCDDemo[62020:76113114] ==== task2 thread = <NSThread: 0x60000176c2c0>{number = 3, name = (null)}
2020-11-16 16:08:33.373147+0800 GCDDemo[62020:76113116] ==== task1 thread = <NSThread: 0x600001767340>{number = 4, name = (null)}
从打印结果可知,三个任务在三条不同的线程上异步执行。因此,当一个并发队列里都是异步执行任务时,会给每一个任务开启一条新线程,来异步执行这些任务。
3. 既有同步执行又有异步执行的任务
如下面代码所示,往一个并发队列中穿插添加两个同步执行的任务和两个异步执行的任务。
//并发队列+同步和异步执行任务
-(void)testConcurrent{
NSLog(@" ==== method thread = %@", [NSThread currentThread]);
NSLog(@" ==== begin");
dispatch_queue_t queue = dispatch_queue_create("", DISPATCH_QUEUE_CONCURRENT);
//添加异步执行任务1
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:4];
NSLog(@" ==== task1 thread = %@", [NSThread currentThread]);
});
//添加同步执行任务2
dispatch_sync(queue, ^{
[NSThread sleepForTimeInterval:3];
NSLog(@" ==== task2 thread = %@", [NSThread currentThread]);
});
//添加异步执行任务3
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@" ==== task3 thread = %@", [NSThread currentThread]);
});
//添加同步执行任务4
dispatch_sync(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@" ==== task4 thread = %@", [NSThread currentThread]);
});
NSLog(@" ==== end");
}
//打印结果
2020-11-16 16:32:19.942009+0800 GCDDemo[63414:76291148] ==== method thread = <NSThread: 0x600001704e80>{number = 1, name = main}
2020-11-16 16:32:19.942079+0800 GCDDemo[63414:76291148] ==== begin
2020-11-16 16:32:22.942793+0800 GCDDemo[63414:76291148] ==== task2 thread = <NSThread: 0x600001704e80>{number = 1, name = main}
2020-11-16 16:32:23.944156+0800 GCDDemo[63414:76291650] ==== task1 thread = <NSThread: 0x600001742b00>{number = 2, name = (null)}
2020-11-16 16:32:24.943348+0800 GCDDemo[63414:76291148] ==== task4 thread = <NSThread: 0x600001704e80>{number = 1, name = main}
2020-11-16 16:32:24.943353+0800 GCDDemo[63414:76291576] ==== task3 thread = <NSThread: 0x600001742b80>{number = 3, name = (null)}
2020-11-16 16:32:24.943470+0800 GCDDemo[63414:76291148] ==== end
从打印结果可知,两个同步任务均在主线程中按序执行,两个异步任务在两个不同的子线程中执行(打印结果的顺序后面再分析)。因此,当一个并发队列中既有同步执行任务,还有异步执行任务时,会给每一个异步任务开启一条新线程来异步执行,而所有同步任务均在当前线程中按顺序执行。
4. 并发队列分析后的结论
根据上面的分析可知,串行队列在各场景下的使用情况如下:
- 当一个并发队列里都是同步执行的任务,不会开辟新线程,所有任务均在当前线程同步执行。
- 当一个并发队列里都是异步执行任务时,会给每一个任务开启一条新线程,来异步执行这些任务
- 当一个并发队列中既有同步执行任务,还有异步执行任务时,会给每一个异步任务开启一条新线程来异步执行,而所有同步任务均在当前线程中按顺序执行。
六、同步函数和异步函数的耗时情况
对相同复杂度的任务来说,dispacth_sync
和dispatch_async
执行时间是不一样的。
- dispacth_async
-(void)testAsync{
CFAbsoluteTime time = CFAbsoluteTimeGetCurrent();
dispatch_queue_t queue = dispatch_queue_create("", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@" ==== async interval = %f", CFAbsoluteTimeGetCurrent() - time);
});
NSLog(@" ==== interval = %f", CFAbsoluteTimeGetCurrent() - time);
}
//打印结果
2020-11-16 19:05:31.131300+0800 GCDDemo[72585:77510232] ==== interval = 0.000019
2020-11-16 19:05:31.131308+0800 GCDDemo[72585:77510854] ==== async interval = 0.000029
从打印结果可以看出,dispatch_async
函数虽然先执行,但执行相同复杂度任务所需的时间,还是比直接执行要久。
- dispacth_async和dispatch_sync
-(void)testAsyncAndSync{
CFAbsoluteTime time = CFAbsoluteTimeGetCurrent();
dispatch_queue_t queue = dispatch_queue_create("", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@" ==== async interval = %f", CFAbsoluteTimeGetCurrent() - time);
});
dispatch_sync(queue, ^{
NSLog(@" ==== sync interval = %f", CFAbsoluteTimeGetCurrent() - time);
});
}
//打印结果
2020-11-16 19:09:08.919239+0800 GCDDemo[72810:77540305] ==== sync interval = 0.000033
2020-11-16 19:09:08.919398+0800 GCDDemo[72810:77540590] ==== async interval = 0.000214
从打印结果可知,执行相同复杂度的任务时,dispatch_async
要比dispatch_sync
所需时间更长。
六、面试题解析
学习了前面的知识后,接下来对几个高频次面试题的输出结果进行解析。
1. 并发队列 + 异步执行
-(void)interview1{
dispatch_queue_t queue = dispatch_queue_create("", DISPATCH_QUEUE_CONCURRENT);
NSLog(@" ==== 1");
dispatch_async(queue, ^{
NSLog(@" ==== 2");
dispatch_async(queue, ^{
NSLog(@" ==== 3");
});
NSLog(@" ==== 4");
});
NSLog(@" ==== 5");
}
//打印结果
2020-11-16 19:17:51.449472+0800 GCDDemo[73315:77606414] ==== 1
2020-11-16 19:17:51.449562+0800 GCDDemo[73315:77606414] ==== 5
2020-11-16 19:17:51.449575+0800 GCDDemo[73315:77606769] ==== 2
2020-11-16 19:17:51.449634+0800 GCDDemo[73315:77606769] ==== 4
2020-11-16 19:17:51.449646+0800 GCDDemo[73315:77606768] ==== 3
这道题比较简单,分析思路:先整体,再局部。
- 首先,从整体上看,外层方法
interview1
是在主线程中执行,里面的操作按顺序执行,主要包含:NSLog(@" ==== 1")
,dispatch_async
、NSLog(@" ==== 5")
。由于dispatch_async
是异步执行无需等待,且比直接执行形同复杂度的任务耗时更久,所以会先输出1,5,再输出dispatch_async
函数block里的打印。 - 再看
dispatch_async
里的任务,里面包含三个操作:NSLog(@" ==== 2")
、dispatch_async
、NSLog(@" ==== 4")
。这三个操作在新开启的子线程里按顺序执行,和interview1
里的操作一样,输出顺序为:2、4、3。
因此,经过分析得知,这道题的输出顺序为:1、5、2、4、3。
这里将并发队列改为串行队列,输出顺序还是一样,大家可以自己分析下。
2. 并发队列 + 异步函数嵌套同步函数
-(void)interview2{
dispatch_queue_t queue = dispatch_queue_create("", DISPATCH_QUEUE_CONCURRENT);
NSLog(@" ==== 1");
dispatch_async(queue, ^{
NSLog(@" ==== 2");
dispatch_sync(queue, ^{
NSLog(@" ==== 3");
});
NSLog(@" ==== 4");
});
NSLog(@" ==== 5");
}
//打印结果
2020-11-16 19:44:38.915026+0800 GCDDemo[74906:77810083] ==== 1
2020-11-16 19:44:38.915117+0800 GCDDemo[74906:77810083] ==== 5
2020-11-16 19:44:38.915127+0800 GCDDemo[74906:77810255] ==== 2
2020-11-16 19:44:38.915168+0800 GCDDemo[74906:77810255] ==== 3
2020-11-16 19:44:38.915201+0800 GCDDemo[74906:77810255] ==== 4
这道题只是在上一题上做个小改动,将里面的dispatch_async
改为dispatch_sync
。所以分析思路还是一样:先整体,再局部。
- 从整体上看,还是先输出1、5,再输出
dispatch_async
函数block里的任务。 - 从
dispatch_async
里的任务来看,三个操作NSLog(@" ==== 2")
、dispatch_sync
以及NSLog(@" ==== 4")
在新开的子线程中同步执行,所以先输出2,而dispatch_sync
是同步函数,所以需要等待其block里的任务执行完,才能执行后面的任务,所以先输出3,再输出4。
因此,经过分析得知,这道题的输出顺序为:1、5、2、3、4。
3. 串行队列 + 异步函数嵌套同步函数
-(void)interview3{
dispatch_queue_t queue = dispatch_queue_create("", DISPATCH_QUEUE_SERIAL);
NSLog(@" ==== 1");
dispatch_async(queue, ^{
NSLog(@" ==== 2");
dispatch_sync(queue, ^{
NSLog(@" ==== 3");
});
NSLog(@" ==== 4");
});
NSLog(@" ==== 5");
}
//打印结果
2020-11-16 20:06:27.263232+0800 GCDDemo[76170:77980426] ==== 1
2020-11-16 20:06:27.263325+0800 GCDDemo[76170:77980426] ==== 5
2020-11-16 20:06:27.263338+0800 GCDDemo[76170:77980465] ==== 2
//报错
Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
dispatch_sync_f_slow
这里输出1、5、2后就报错,因为任务互相等待,阻塞了串行队列queue,导致子线程死锁了(具体分析查在上文的『普通串行队列死锁』)。
4. 并发队列 + 异步函数嵌套同步函数
-(void)interview4{
dispatch_queue_t queue = dispatch_queue_create("", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@" ==== 1");
});
dispatch_async(queue, ^{
NSLog(@" ==== 2");
});
dispatch_sync(queue, ^{
NSLog(@" ==== 3");
});
NSLog(@" ==== 0");
dispatch_async(queue, ^{
NSLog(@" ==== 7");
});
dispatch_async(queue, ^{
NSLog(@" ==== 8");
});
dispatch_async(queue, ^{
NSLog(@" ==== 9");
});
}
//打印结果
2020-11-16 21:36:24.898847+0800 GCDDemo[81805:78779526] ==== 3
2020-11-16 21:36:24.898907+0800 GCDDemo[81805:78779885] ==== 2
2020-11-16 21:36:24.898880+0800 GCDDemo[81805:78779883] ==== 1
2020-11-16 21:36:24.898930+0800 GCDDemo[81805:78779526] ==== 0
2020-11-16 21:36:24.898997+0800 GCDDemo[81805:78779883] ==== 7
2020-11-16 21:36:24.899024+0800 GCDDemo[81805:78779885] ==== 8
2020-11-16 21:36:24.899045+0800 GCDDemo[81805:78779883] ==== 9
这里的输出结果只是其中的一种情况,还有其他情况。从前面对并发队列分析后的结论可知,当一个并发队列中既有同步执行任务,还有异步执行任务时,会给每一个异步任务开启一条新线程来异步执行,而所有同步任务均在当前线程中按顺序执行。因此这题的分析思路:先同步,再异步。
-
3
和0
的输出是处于外层方法interview4
中的同步执行任务,所以0必定会在3后输出。 -
7
、8
、9
三个输出任务的dispatch_async
操作是在外层方法interview4
中同步执行的,且在同步任务NSLog(@" ==== 0")
后面,所以7、8、9必定在0后输出。 -
1
、2
、7
、8
、9
这五个输出任务均在并发队列queue中,且会在不同的子线程上异步执行,不需要等待,所以它们的执行不分先后。
因此,经过分析可知,0必定在3的后面输出,7、8、9必定在0的后面输出,而根据任务的复杂度不同,1,2在任何位置出现都有可能。