iOS GCD之任务与队列
什么是GCD?
Grand Central Dispatch(GCD)是 Apple 开发的一个多核编程的解决方法。它是一套纯 C
语言的 API
,主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。它是一个在线程池模式的基础上执行的并发任务。在 Mac OS X 10.6 雪豹中首次推出,也可在 iOS 4 及以上版本使用。
为什么要用GCD?有什么优势?
- GCD 可用于多核的并行运算
- GCD 会自动利用更多的 CPU 内核(比如双核、四核)
- GCD 会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
- 程序员只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码
核心
在学习使用之前,先介绍两个核心概念:任务和队列
任务
就是 执行操作
的意思,换句话说就是在线程中执行的那段代码,在 GCD
中是放在 block
中的。执行任务有两种方式:同步执行(sync)和异步执行(async)
- 异步执行(async)
异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。可以在新的线程中执行任务,具备开启新线程的能力。
- 同步执行(sync)
同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。只能在当前线程中执行任务,不具备开启新线程的能力。
异步执行(async)虽然具有开启新线程的能力,但是并不一定开启新线程。这跟任务所指定的队列类型有关
队列(Dispatch Queue)
这里的队列指执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的线性表,采用 FIFO
(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务。

在 GCD
中,有两种队列:串行队列(Serial Dispatch Queue)和并发队列(Concurrent Dispatch Queue)
- 串行队列(Serial Dispatch Queue)
每次只有一个任务被执行。让任务一个接着一个地执行。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)

- 并发队列(Concurrent Dispatch Queue)
可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务)

并发队列的并发功能只有在异步函数下才有效
使用步骤
GCD 的使用非常简单,只有两步
- 创建一个队列(串行队列/并发队列)
- 将任务添加到队列中,然后系统就会根据任务类型执行任务(同步执行或异步执行)
队列的创建方法
可以通过 dispatch_queue_create
来创建队列,源码如下:
API_AVAILABLE(macos(10.6), ios(4.0))
DISPATCH_EXPORT DISPATCH_MALLOC DISPATCH_RETURNS_RETAINED DISPATCH_WARN_RESULT
DISPATCH_NOTHROW
dispatch_queue_t
dispatch_queue_create(const char *_Nullable label,
dispatch_queue_attr_t _Nullable attr);
可以看到,需要传入两个参数:第一个参数表示队列的唯一标识符,用于 DEBUG,可为空;第二个参数用来识别是串行队列还是并发队列。DISPATCH_QUEUE_SERIAL
表示串行队列,DISPATCH_QUEUE_CONCURRENT
表示并发队列。
- 创建一个串行队列
dispatch_queue_t seialQueue = dispatch_queue_create("xxx", DISPATCH_QUEUE_SERIAL);
- 创建一个并发队列
dispatch_queue_t concurrentQueue = dispatch_queue_create("xxx", DISPATCH_QUEUE_CONCURRENT);
队列的获取方法
GCD
提供了一个特殊的串行队列:主队列(Main Dispatch Queue)。所有放在主队列中的任务,都会放到主线程中执行。获取方法如下:
dispatch_queue_t mainQueue = dispatch_get_main_queue();
此外,GCD
还提供了一个全局并发队列:(Global Dispatch Queue)
API_AVAILABLE(macos(10.6), ios(4.0))
DISPATCH_EXPORT DISPATCH_CONST DISPATCH_WARN_RESULT DISPATCH_NOTHROW
dispatch_queue_global_t
dispatch_get_global_queue(intptr_t identifier, uintptr_t flags);
可以看到,获取全局队列也需要传入两个参数。第一个参数表示队列优先级,一般用DISPATCH_QUEUE_PRIORITY_DEFAULT。第二个参数暂时没用,用 0 即可。
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
任务的创建方法
GCD
提供了两种执行任务的创建方法:
// 同步执行任务创建方法
dispatch_sync(queue, ^{
// 同步执行任务代码
});
// 异步执行任务创建方法
dispatch_async(queue, ^{
// 异步执行任务代码
});
综上所述,GCD
为开发者提供了两种队列(串行/并发)以及两种任务执行方法(同步/异步),所以这里面就有了四种组合。再结合上面提到的主队列和全局并发队列(可以当作是普通的并发队列),现在就有了6种组合:
区别 | 主队列 | 串行队列 | 并发队列 |
---|---|---|---|
同步 | 没有开启新线程,死锁 | 没有开启新线程,顺序执行任务 | 没有开启新线程,顺序执行任务 |
异步 | 没有开启新线程,顺序执行任务 | 有开启新线程(1条),顺序执行任务 | 有开启新线程,并发执行任务 |
这里我们先给出不同组合的区别,下面详细讲解
基本使用
下面分别介绍下并发队列和串行队列的两种执行方式
并发队列+同步执行
在当前线程中执行任务,不会开启新的线程,执行完一个任务,再执行另一个任务
- (void)syncConcurrent {
NSLog(@"当前线程:%@", [NSThread currentThread]);
NSLog(@"并发队列+同步执行---开始");
dispatch_queue_t queue = dispatch_queue_create("sync.concurrent", DISPATCH_QUEUE_CONCURRENT);
dispatch_sync(queue, ^{
//任务1
for (int i = 0; i < 2; i++) {
sleep(2); //模拟耗时操作
NSLog(@"任务1所在的线程:%@", [NSThread currentThread]);
}
});
dispatch_sync(queue, ^{
//任务2
for (int i = 0; i < 2; i++) {
sleep(2); //模拟耗时操作
NSLog(@"任务2所在的线程:%@", [NSThread currentThread]);
}
});
dispatch_sync(queue, ^{
//任务3
for (int i = 0; i < 2; i++) {
sleep(2); //模拟耗时操作
NSLog(@"任务3所在的线程:%@", [NSThread currentThread]);
}
});
NSLog(@"并发队列+同步执行---结束");
}
运行项目,输出结果如下:

可以看到:
- 所有任务都是在当前线程(主线程)中执行的,没有开启新的线程(同步执行不具备开启新线程的能力)
- 所有任务的打印都是在
并发队列+同步执行---开始
之后,并发队列+同步执行---结束
之前(同步任务需要等待队列的任务执行结束之后才能执行) - 任务是按照先后顺序执行的。虽然并发队列可以开启多个线程,并且同时执行多个任务,但是同步任务不具备开启新线程的能力,所以只有当前线程这一个线程,也就不存在并发。当前线程只有等待当前队列中正在执行的任务执行完毕,才能继续执行下一步操作,所以任务只能一个个按照顺序执行。
并发队列+异步执行
可以开启多个线程,任务交替执行(即我们肉眼看到的同时执行)
- (void)asyncConcurrent {
NSLog(@"当前线程:%@", [NSThread currentThread]);
NSLog(@"并发队列+异步执行---开始");
dispatch_queue_t queue = dispatch_queue_create("async.concurrent", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
//任务1
for (int i = 0; i < 2; i++) {
sleep(2); //模拟耗时操作
NSLog(@"任务1所在的线程:%@", [NSThread currentThread]);
}
});
dispatch_async(queue, ^{
//任务2
for (int i = 0; i < 2; i++) {
sleep(2); //模拟耗时操作
NSLog(@"任务2所在的线程:%@", [NSThread currentThread]);
}
});
dispatch_async(queue, ^{
//任务3
for (int i = 0; i < 2; i++) {
sleep(2); //模拟耗时操作
NSLog(@"任务3所在的线程:%@", [NSThread currentThread]);
}
});
NSLog(@"并发队列+异步执行---结束");
}
运行项目,输出结果如下:

可以看到:
- 除了当前线程(主线程),系统重新开辟了 3 个线程(异步执行具备开启新线程的能力)
- 任务是交替执行的(并发队列可开启多个线程,
同时
执行多个任务) - 所有任务的执行都是在
并发队列+异步执行---开始
和并发队列+异步执行---结束
之后,说明了当前线程并没有等待,而是直接开启了新的线程,在新的线程中执行任务(异步执行不做等待,可以继续执行任务)
串行队列+异步执行
具备开启新线程的能力,但是因为任务是在串行队列中执行,当前任务执行完毕,才会执行下一个任务
- (void)asyncSerial {
NSLog(@"当前线程:%@", [NSThread currentThread]);
NSLog(@"串行队列+异步执行---开始");
dispatch_queue_t queue = dispatch_queue_create("async.serial", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
//任务1
for (int i = 0; i < 2; i++) {
sleep(2); //模拟耗时操作
NSLog(@"任务1所在的线程:%@", [NSThread currentThread]);
}
});
dispatch_async(queue, ^{
//任务2
for (int i = 0; i < 2; i++) {
sleep(2); //模拟耗时操作
NSLog(@"任务2所在的线程:%@", [NSThread currentThread]);
}
});
dispatch_async(queue, ^{
//任务3
for (int i = 0; i < 2; i++) {
sleep(2); //模拟耗时操作
NSLog(@"任务3所在的线程:%@", [NSThread currentThread]);
}
});
NSLog(@"串行队列+异步执行---结束");
}
运行项目,输出结果如下:

可以看到:
- 开启了新的线程(异步执行具备开启新线程的能力,串行队列只开启一个新线程)
- 所有任务都是在
串行队列+异步执行---开始
和串行队列+异步执行---结束
之后开始执行的(异步执行不做任何等待,可以继续执行) - 任务是按照先后顺序执行的(串行队列每次只有一个任务被执行,任务一个接一个顺序执行)
串行队列+同步执行
不会开启新的线程,任务会顺序执行的。
- (void)syncSerial {
NSLog(@"当前线程:%@", [NSThread currentThread]);
NSLog(@"串行队列+同步执行---开始");
dispatch_queue_t queue = dispatch_queue_create("sync.serial", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
//任务1
for (int i = 0; i < 2; i++) {
sleep(2); //模拟耗时操作
NSLog(@"任务1所在的线程:%@", [NSThread currentThread]);
}
});
dispatch_sync(queue, ^{
//任务2
for (int i = 0; i < 2; i++) {
sleep(2); //模拟耗时操作
NSLog(@"任务2所在的线程:%@", [NSThread currentThread]);
}
});
dispatch_sync(queue, ^{
//任务3
for (int i = 0; i < 2; i++) {
sleep(2); //模拟耗时操作
NSLog(@"任务3所在的线程:%@", [NSThread currentThread]);
}
});
NSLog(@"串行队列+同步执行---结束");
}
运行项目,输出结果如下:

可以看到:
- 所有任务都是在当前线程(主线程)中执行的,没有开启新的线程(同步执行不具备开启新线程的能力)
- 所有任务都是在
串行队列+同步执行---开始
之后,串行队列+同步执行---结束
之前(同步任务需要等待队列的任务执行结束之后才能执行) - 任务是按照顺序先后执行的(串行队列每次只有一个任务被执行,执行完一个任务,才会执行下一步操作)
主队列
是 GCD
自带的一种特殊的队列:
- 所有放在主队列中的任务,都会放到主线程中执行
- 获取:
dispatch_get_main_queue()
主队列也有两种组合方式
主队列+异步执行
只在主线程中执行任务,执行完一个任务,再执行下一个任务
- (void)asyncMain {
NSLog(@"当前线程:%@", [NSThread currentThread]);
NSLog(@"主队列+异步执行---开始");
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
//任务1
for (int i = 0; i < 2; i++) {
sleep(2); //模拟耗时操作
NSLog(@"任务1所在的线程:%@", [NSThread currentThread]);
}
});
dispatch_async(queue, ^{
//任务2
for (int i = 0; i < 2; i++) {
sleep(2); //模拟耗时操作
NSLog(@"任务2所在的线程:%@", [NSThread currentThread]);
}
});
dispatch_async(queue, ^{
//任务3
for (int i = 0; i < 2; i++) {
sleep(2); //模拟耗时操作
NSLog(@"任务3所在的线程:%@", [NSThread currentThread]);
}
});
NSLog(@"主队列+异步执行---结束");
}
运行项目,输出结果如下:

可以看到:
- 所有任务都是在主线程中执行的,并没有开启新线程(异步执行具备开启新线程的能力,但因为在主队列,所有任务都是在主线程中)
- 所有任务都是在
主队列+异步执行---开始
和主队列+异步执行---结束
之后执行(异步执行不做任何等待,可以继续执行任务) - 任务是按照顺序执行的(主队列是串行队列,每次只有一个任务被执行,一个任务执行完毕,才会执行下一个任务)
主队列+同步执行
同步执行 + 主队列在不同线程中调用结果也是不一样,在主线程中调用会出现死锁,而在其他线程中则不会。
- 在主线程中调用
主队列+同步执行
互相等待(死锁)

可以看到:
程序崩溃了,因为我们在主线程中执行 syncMain
方法,相当于把 syncMain
放到了主线程的队列中。同步执行会等待当前队列中的任务执行完毕,才会继续执行。当我们把任务追加到主队列中,任务 1 就在等待主线程处理完 syncMain
任务,而 syncMain
任务需要等待任务 1 执行完毕,才能继续执行。两个任务相互等待,卡住(死锁)后续都执行不了
- 在其他线程中调用
主队列+同步执行
不会开启新线程,执行完一个任务,继续执行下一个任务
// 使用 NSThread 的 detachNewThreadSelector 方法会创建线程,并自动启动线程执行
[NSThread detachNewThreadSelector:@selector(syncMain) toTarget:self withObject:nil];
- (void)syncMain {
NSLog(@"当前线程:%@", [NSThread currentThread]);
NSLog(@"在其他线程中主队列+同步执行---开始");
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
//任务1
for (int i = 0; i < 2; i++) {
sleep(2); //模拟耗时操作
NSLog(@"任务1所在的线程:%@", [NSThread currentThread]);
}
});
dispatch_sync(queue, ^{
//任务2
for (int i = 0; i < 2; i++) {
sleep(2); //模拟耗时操作
NSLog(@"任务2所在的线程:%@", [NSThread currentThread]);
}
});
dispatch_sync(queue, ^{
//任务3
for (int i = 0; i < 2; i++) {
sleep(2); //模拟耗时操作
NSLog(@"任务3所在的线程:%@", [NSThread currentThread]);
}
});
NSLog(@"在其他线程中主队列+同步执行---结束");
}
运行项目,输出结果如下:

可以看到:
- 所有任务都是在主线程(非当前线程)中执行的,没有开启新的线程(所有放在主队列中的任务,都会放到主线程中执行)
- 所有任务都是在
在其他线程中主队列+同步执行---开始
之后,在其他线程中主队列+同步执行---结束
之前执行(同步任务需要等待队列的任务执行结束) - 任务是按顺序执行的(主队列是串行队列,每次只有一个任务被执行,任务一个接一个按顺序执行)
syncMain
任务放到了其他线程中执行,当任务1、任务2、任务3追加到主队列中,这三个任务都会在主线程中执行。syncMain
任务在其他线程中执行到追加任务1到主队列中,因为主队列现在没有正在执行的任务,所以,会直接执行主队列的任务1,等任务1执行完毕,再接着执行任务2、任务3。所以这里不会卡住线程