文章目录
基础知识
什么是GCD?以下摘自苹果的官方说明。
Grand Central Dispatch(GCD) 是 异步执行任务的技术之一。一般将应用的程序中记述的线程管理用的代码在系统级中的实现。开发者只需要定义执行的任务并追加到适当的Dispatch Queue中,GCD就能生成必要的线程并计划执行任务,由于线程管理是作为系统的一部分来实现的,因此可统一管理,也可执行任务,这样就比以前线程更有效率。
任务和队列
学习 GCD 之前,先来了解 GCD 中两个核心概念:任务和队列。
任务
就是执行操作的意思,换句话说就是你在线程中执行的那段代码。在 GCD 中是放在 block 中的。执行任务有两种方式:同步执行 和 异步执行。
- 两者的主要区别是:是否等待队列的任务执行结束,以及是否具备开启新线程的能力。
同步执行(sync)
- 同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。
- 只能在当前线程中执行任务,不具备开启新线程的能力。
异步执行(async)
- 异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。
- 可以在新的线程中执行任务,具备开启新线程的能力。
队列(Dispatch Queue)
队列(Dispatch Queue)里的队列指执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的线性表,采用 FIFO(先进先出) 的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务。
- 队列的结构可参考下图:
在 GCD 中有两种队列:串行队列 和 并发队列。两者都符合 FIFO(先进先出) 的原则。两者的主要区别是:执行顺序不同,以及开启线程数不同。
串行队列(Serial Dispatch Queue)
- 每次只有一个任务被执行。让任务一个接着一个地执行。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)
并发队列(Concurrent Dispatch Queue)
- 可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务)
两者具体区别如下两图所示:
同步异步、串行并行和线程的关系
由上图我们可以得出我们有3种队列,2种任务执行方式,那么我们就有了6种不同的组合方式,分别是:
- 并行队列 + 同步执行
- 并行队列 + 异步执行
- 串行队列 + 同步执行
- 串行队列 + 异步执行
- 主队列 + 同步执行
- 主队列 + 异步执行
那么这几种不同组合方式各有什么区别:
队列 | 并发队列 | 串行队列 | 主队列 |
---|---|---|---|
同步 (sync) | 没有开启新线程,串行执行任务 | 没有开启新线程,串行执行任务 | 主线程调用:死锁卡住不执行其他线程调用:没有开启新线程,串行执行任务 |
异步 (async) | 有开启新线程,并发执行任务 | 有开启新线程(1条),串行执行任务 | 没有开启新线程,串行执行任务 |
需要注意的几个地方:
- 异步执行(async) 虽然具有开启新线程的能力,但是并不一定开启新线程。
- 并发队列的并发功能只有在异步(
dispatch_async
)函数下才有效。 - 主队列是一种特殊的串行队列。
GCD
首先回顾一下苹果官方对GCD的说明。
- 开发者要做的只是定义想执行的任务并追加到适当的Dispatch Queue中。
这句话用源代码表示如下:
dispatch_async(queue, ^ {
/*
* 想执行的任务
*/
});
该源代码使用Block语法“定义想执行的任务”,通过dispatch_async
函数“追加”赋值在变量queue
的“Dispatch Queue中”。今这样就可使指定的Block
在另一线程中执行。
“Dispatch Queue” 是什么呢?
- 如其名称所示,是执行处理的等待队列。应用程序编程人员通过
dispatch_async
函数等API,在Block语法中记述想执行的处理并将其追加到Dispatch Queue中。Dispatch Queue按照追加的顺序 (先进先出FIFO,First-In-First-Out) 执行处理。
另外在执行处理时存在两种Dispatch Queue,一种是等待现在执行中处理的Serial Dispatch Queue,另一种是不等待现在执行中处理的Concurrent Dispatch Queue。如下所示。
比较这两种Dispatch Queue。准备以下源代码,在dispatch_async
中追加多个处理。
dispatch_async(queue, blk0);
dispatch_async(queue, blk1);
dispatch_async(queue, blk2);
dispatch_async(queue, blk3);
dispatch_async(queue, blk4);
dispatch_async(queue, blk5);
dispatch_async(queue, blk6);
dispatch_async(queue, blk7);
当变量queue
为Serial Dispatch Queue时,因为要等待现在执行中的处理结束,所以首先执行blk0
,blk0
执行结束后,接着执行blk1
,blk1
结束后再开始执行blk2
,如此重复。同时执行的处理数只能有1个。即执行该源代码后,一定按照以下顺序进行处理。
blk0
blk1
blk2
blk3
blk4
blk5
blk6
blk7
当变量queue
为Concurrent Dispatch Queue 时,因为不用等待现在执行中的处理结束,所以首先执行blk0
,不管blk0
的执行是否结束,都开始执行后面的blk1
,不管blk1
的执行是否结束,都开始执行后面的blk2
,如此重复循环。
这样虽然不用等待处理结束,可以并行执行多个处理,但并行执行的处理数量取决于当前系统的状态。即 iOS和OS X基于Dispatch Queue中的处理数、处理CPU核数以及CPU负荷等当前系统的状态来决定Concurrent Dispatch Queue中并行执行的处理数。所谓“并行执行”,就是使用多个线程同时执行多个处理。如下图所示。
GCD的使用步骤
队列的创建
可以使用dispatch_queue_create
来创建对象,需要传入两个参数:
- 第一个参数表示队列的唯一标识符,用于DEBUG,可为空;
- 第二个参数用来识别是串行队列还是并行队列。
DISPATCH_QUEUE_SERIAL
表示串行队列,DISPATCH_QUEUE_CONCURRENT
表示并行队列。
// 串行队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("net.test.queue", DISPATCH_QUEUE_SERIAL);
// 并行队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("net.test.queue", DISPATCH_QUEUE_CONCURRENT);
// 全局并行队列。GCD默认提供了全局的并行队列,需要传入两个参数。第一个参数表示队列优先级,一般用DISPATCH_QUEUE_PRIORITY_DEFAULT。第二个参数暂时没用,用0即可
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
});
队列的获取
- 队列的获取(这里说的是两个特殊的队列,主队列和全局并发队列):
//主队列:
dispatch_queue_t queue = dispatch_get_main_queue();
//并发队列:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
任务的创建
// 同步执行任务创建方法
dispatch_sync(queue, ^{
// 执行的任务
NSLog(@"%@",[NSThread currentThread]);
});
// 异步执行任务创建方法
dispatch_async(queue, ^{
// 执行的任务
NSLog(@"%@",[NSThread currentThread]);
});
需要注意的几个地方:
- 所有放在主队列中的任务,都会放到主线程中执行。
- GCD 默认提供了全局并发队列(Global Dispatch Queue)。
GCD的基本使用
同步执行 并发队列
- 在当前线程中执行任务,不会开启新线程,执行完一个任务,再执行下一个任务。
/**
* 同步执行 并发队列
* 特点:在当前线程中执行任务,不会开启新线程,执行完一个任务,再执行下一个任务。
*/
- (void)syncConcurrent {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"syncConcurrent---begin");
dispatch_queue_t queue = dispatch_queue_create("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");
}
运行结果:
从同步执行 并发队列中可看到:
- 所有任务都是在当前线程(主线程)中执行的,没有开启新的线程(同步执行不具备开启新线程的能力)。
- 所有任务都在打印的
syncConcurrent---begin
和syncConcurrent---end
之间执行的(同步任务需要等待队列的任务执行结束)。 - 任务按顺序执行的。按顺序执行的原因:虽然并发队列可以开启多个线程,并且同时执行多个任务。但是因为本身不能创建新线程,只有当前线程这一个线程(同步任务不具备开启新线程的能力),所以也就不存在并发。而且当前线程只有等待当前队列中正在执行的任务执行完毕之后,才能继续接着执行下面的操作(同步任务需要等待队列的任务执行结束)。所以任务只能一个接一个按顺序执行,不能同时被执行。
异步执行 并发队列
- 可以开启多个线程,任务交替(同时)执行。
/**
* 异步执行 并发队列
* 特点:可以开启多个线程,任务交替(同时)执行。
*/
- (void)asyncConcurrent {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"asyncConcurrent---begin");
dispatch_queue_t queue = dispatch_queue_create("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");
}
运行结果:
在异步执行 并发队列中可以看出:
- 除了当前线程(主线程),系统又开启了3个线程,并且任务是交替/同时执行的。(异步执行具备开启新线程的能力。且并发队列可开启多个线程,同时执行多个任务)。
- 所有任务是在打印的
syncConcurrent---begin
和syncConcurrent---end
之后才执行的。说明当前线程没有等待,而是直接开启了新线程,在新线程中执行任务(异步执行不做等待,可以继续执行任务)。
同步执行 串行队列
不会开启新线程,在当前线程执行任务。任务是串行的,执行完一个任务,再执行下一个任务。
/**
* 同步执行 串行队列
* 特点:不会开启新线程,在当前线程执行任务。任务是串行的,执行完一个任务,再执行下一个任务。
*/
- (void)syncSerial {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"syncSerial---begin");
dispatch_queue_t queue = dispatch_queue_create("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");
}
运行结果:
在同步执行 串行队列可以看到:
- 所有任务都是在当前线程(主线程)中执行的,并没有开启新的线程(同步执行不具备开启新线程的能力)。
- 所有任务都在打印的
syncConcurrent---begin
和syncConcurrent---end
之间执行(同步任务需要等待队列的任务执行结束)。 - 任务是按顺序执行的(串行队列每次只有一个任务被执行,任务一个接一个按顺序执行)。
异步执行 串行队列
- 会开启新线程,但是因为任务是串行的,执行完一个任务,再执行下一个任务
- (void)asyncSerial {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"asyncSerial---begin");
dispatch_queue_t queue = dispatch_queue_create("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");
}
运行结果:
在异步执行 串行队列可以看到:
- 开启了一条新线程(异步执行具备开启新线程的能力,串行队列只开启一个线程)。
- 所有任务是在打印的
syncConcurrent---begin
和syncConcurrent---end
之后才开始执行的(异步执行不会做任何等待,可以继续执行任务)。 - 任务是按顺序执行的(串行队列每次只有一个任务被执行,任务一个接一个按顺序执行)。
同步执行 主队列
同步执行 主队列在不同线程中调用结果也是不一样,在主线程中调用会出现死锁,而在其他线程中则不会。
在主线程中调用同步执行 主队列
- 互相等待卡住不可行
/**
* 同步执行 主队列
* 特点(主线程调用):互等卡主不执行。
* 特点(其他线程调用):不会开启新线程,执行完一个任务,再执行下一个任务。
*/
- (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");
}
运行结果:
在同步执行 主队列可以惊奇的发现:
在主线程中使用同步执行 主队列,追加到主线程的任务1、任务2、任务3都不再执行了,而且syncMain---end
也没有打印,在XCode上还会报崩溃。这是为什么呢?
- 这是因为我们在主线程中执行
syncMain
方法,相当于把syncMain
任务放到了主线程的队列中。而同步执行会等待当前队列中的任务执行完毕,才会接着执行。那么当我们把任务1追加到主队列中,任务1就在等待主线程处理完syncMain
任务。而syncMain
任务需要等待任务1执行完毕,才能接着执行。 - 那么,现在的情况就是
syncMain
任务和任务1都在等对方执行完毕。这样大家互相等待,所以就卡住了,所以我们的任务执行不了,而且syncMain---end
也没有打印。
在其他线程中调用同步执行 主队列
- 不会开启新线程,执行完一个任务,再执行下一个任务
- (void)viewDidLoad {
[super viewDidLoad];
// 使用 NSThread 的 detachNewThreadSelector 方法会创建线程,并自动启动线程执行selector 任务
[NSThread detachNewThreadSelector:@selector(syncMain) toTarget:self withObject:nil];
}
- (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");
}
运行结果:
在其他线程中使用同步执行 主队列可看到:
- 所有任务都是在主线程(非当前线程)中执行的,没有开启新的线程(所有放在主队列中的任务,都会放到主线程中执行)。
- 所有任务都在打印的
syncConcurrent---begin
和syncConcurrent---end
之间执行(同步任务需要等待队列的任务执行结束)。 - 任务是按顺序执行的(主队列是串行队列,每次只有一个任务被执行,任务一个接一个按顺序执行)。
为什么现在就不会卡住了呢?
- 因为使用
NSThread
的detachNewThreadSelector
方法会创建线程,并自动启动线程执行selector
任务,syncMain
任务放到了其他线程里,而任务1、任务2、任务3都在追加到主队列中,这三个任务都会在主线程中执行。syncMain
任务在其他线程中执行到追加任务1到主队列中,因为主队列现在没有正在执行的任务,所以,会直接执行主队列的任务1,等任务1执行完毕,再接着执行任务2、任务3。所以这里不会卡住线程。
异步执行 主队列
- 只在主线程中执行任务,执行完一个任务,再执行下一个任务。
/**
* 异步执行 主队列
* 特点:只在主线程中执行任务,执行完一个任务,再执行下一个任务
*/
- (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");
}
运行结果:
在异步执行 主队列可以看到:
- 所有任务都是在当前线程(主线程)中执行的,并没有开启新的线程(虽然异步执行具备开启线程的能力,但因为是主队列,所以所有任务都在主线程中)。
- 所有任务是在打印的
syncConcurrent---begin
和syncConcurrent---end
之后才开始执行的(异步执行不会做任何等待,可以继续执行任务)。 - 任务是按顺序执行的(因为主队列是串行队列,每次只有一个任务被执行,任务一个接一个按顺序执行)。
dispatch_set_target_queue
dispatch_queue_create
函数生成的Dispatch Queue不管是Serial Dispatch Queue还是Concurrent Dispatch Queue,都使用与默认优先级Global Dispatch Queue相同执行优先级的线程。而变更生成的Dispatch Queue的执行优先级要使用dispatch_set_target_queue
函数。在后台执行动作处理的Serial Dispatch Queue的生成方法如下:
指定要变更执行优先级的Dispatch Queue为dispatch_set_target_queue
函数的第一个参数,指定与要使用的执行优先级的Global Dispatch Queue为第二个参数(目标)。第一个参数如果指定系统提供的Main Dispatch Queue和Global Dispatch Queue则不知道会出现什么状况,因此这些均不可指定。
将Dispatch Queue指定为dispatch_set_target_queue
函数的参数,不仅可以变更Dispatch Queue的执行优先级,还可以作成Dispatch Queue的执行阶层。如果在多个Serial Dispatch Queue中用dispatch_set_target_queue
函数指定目标为某一个Serial Dispatch Queue,那么原先本应并行执行的Serial Dispatch Queue,在目标Serial Dispatch Queue,那么原本应并行执行的多个Serial Dispatch Queue,在目标Serial Dispatch Queue上只能同时执行一个处理。
在必须将不可并行的处理追加到多个Serial Dispatch Queue中时,如果使用dispatch_set_target_queue
函数将目标指定为某一个Serial Dispatch Queue,即可防止处理并行执行。
dispatch_after
经常会有这样的情况:想再3秒后执行处理。可能不仅限于3秒,总之,这种想在指定时间后执行处理的情况,可食用dispatch_after函数来实现。
在3秒后将指定的Block追加到Main Dispatch Queue中的源代码如下:
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull * NSEC_PER_SEC);
dispatch_after(time, dispatch_get_main_queue(), ^{
NSLog(@"waited at least three seconds.");
})
需要注意的是,dispatch_after
函数并不是在指定时间后执行处理,而只是在指定时间追加处理到Dispatch Queue。此源代码与在3秒后用dispatch_async
函数追加Block到Main Dispatch Queue的相同。
因为Main Dispatch Queue在主线程的Runloop中执行,所以再比如每隔1/60秒执行的Runloop中,Block最快在3秒后执行,最慢在3秒+1/60秒后执行,并且在Main Dispatch Queue有大量处理追加或主线程的处理本身有延迟时,这个时间会更长。
虽然在有严格时间的要求下使用时会出现问题,但在想大致延迟执行处理时,该函数是非常有效的。
- 另外,第二个参数指定要追加处理的Dispatch Queue,第三个参数指定记述要执行处理的Block。
- 第一个参数是指定时间用的
dispatch_time_t
类型的值。该值使用dispatch_time
函数或dispatch_walltime
函数作成。
dispatch_time
函数能够获取从第一个参数dispatch_time_t
类型之中指定的时间开始,到第二个参数指定的毫微秒单位时间后的时间。第一个参数经常使用的值是之前源代码中出现的DISPATCH_TIME_NOW
。这表示现在的时间。即以下源代码可得到表示从现在开始1秒后的dispatch_time_t
类型的值。
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 1ull * NSEC_PER_SEC);
数值和NSEC_PER_SEC
的乘积的到单位为毫微秒的数值。“ull
”是c语言的数值字面量,是显式表明类型时使用的字符串(表示“unsigned long long”)。如果使用NSEC_PER_MSEC
则可以以毫秒为单位计算。以下源代码获取表示从现在开始150毫秒后时间的值。
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 150ull * NSEC_PER_SEC);
dispatch_walltime
函数由POSIX中使用的struct timespec
类型的时间得到dispatch_time_t
类型的值。dispatch_time
函数通常用于计算相对时间,而dispatch_walltime
函数用于计算绝对时间。例如在dispatch_after
函数中想指定2011年11月11日11时11分11秒这一绝对时间的情况,这可作为粗略的闹钟功能使用。
dispatch_once
- 一次性代码(只执行一次):dispatch_once
我们在创建单例、或者有整个程序运行过程中只执行一次的代码时,我们就用到了 GCD 的 dispatch_once
函数。使用dispatch_once
函数能保证某段代码在程序运行过程中只被执行1次,并且即使在多线程的环境下,dispatch_once
也可以保证线程安全。
下面这种经常出现的用来进行初始化的源代码可通过dispatch_once
函数简化。
- (void)once {
static int initialized = NO;
if (initialized == NO) {
/*
*初始化
*/
initialized = YES;
}
}
如果使用dispatch_once
函数,则源代码写为:
/**
* 一次性代码(只执行一次)dispatch_once
*/
- (void)once {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 只执行1次的代码(这里面默认是线程安全的)
});
}
- 源代码看起来没有太大的变化,但是通过
dispatch_once
函数,该源代码即使在多线程环境下执行,也可保证百分之百安全。
之前的源代码在大多数情况下也是安全的。但是在多核CPU中,在正在更新表示是否初始化的标志变量时读取,就有可能多次执行初始化处理。而用dispatch_once
函数初始化就不必担心这样的问题。这就是所说的单例模式,在生成单例对象时使用。
dispatch_group
- GCD 的队列组:dispatch_group
在追加到Dispatch Queue中的多个处理全部结束后想执行结束处理,这种情况会经常出现。只使用一个Serial Dispatch Queue时,只要将想执行的处理全部追加到该Serial Dispatch Queue中并在最后追加结束处理,即可实现。但是在使用Concurrent Dispatch Queue时或同时使用多个Dispatch Queue时,源代码就会变得颇为复杂。
有时候我们会有这样的需求:分别异步执行2个耗时任务,然后当2个耗时任务都执行完毕后再回到主线程执行任务。这时候我们可以用到 GCD 的队列组。
- 调用队列组的
dispatch_group_async
先把任务放到队列中,然后将队列放入队列组中。或者使用队列组的dispatch_group_enter
、dispatch_group_leave
组合 来实现dispatch_group_async
。 - 调用队列组的
dispatch_group_notify
回到指定线程执行任务。或者使用dispatch_group_wait
回到当前线程继续向下执行(会阻塞当前线程)。
dispatch_group_notify
- 监听
group
中任务的完成状态,当所有的任务都执行完成后,追加任务到group
中,并执行任务。
/**
* 队列组 dispatch_group_notify
*/
- (void)groupNotify {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"group---begin");
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 追加任务1
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
}
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 追加任务2
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
}
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 等前面的异步任务1、任务2都执行完毕后,回到主线程执行下边任务
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程
}
NSLog(@"group---end");
});
}
运行结果:
从dispatch_group_notify
相关代码运行输出结果可以看出:
- 当所有任务都执行完成之后,才执行
dispatch_group_notify
block 中的任务。
dispatch_group_wait
- 暂停当前线程(阻塞当前线程),等待指定的
group
中的任务执行完成后,才会往下继续执行。
/**
* 队列组 dispatch_group_wait
*/
- (void)groupWait {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"group---begin");
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 追加任务1
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
}
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 追加任务2
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
}
});
// 等待上面的任务全部完成后,会往下继续执行(会阻塞当前线程)
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"group---end");
}
运行结果:
从dispatch_group_wait
相关代码运行输出结果可以看出:
- 当所有任务执行完成之后,才执行
dispatch_group_wait
之后的操作。但是,使用dispatch_group_wait
会阻塞当前线程。
dispatch_group_enter 、 dispatch_group_leave
dispatch_group_enter
标志着一个任务追加到group
,执行一次,相当于group
中未执行完毕任务数 1dispatch_group_leave
标志着一个任务离开了group
,执行一次,相当于group
中未执行完毕任务数-1。
当 group
中未执行完毕任务数为0的时候,才会使dispatch_group_wait
解除阻塞,以及执行追加到dispatch_group_notify
中的任务。
/**
* 队列组 dispatch_group_enter、dispatch_group_leave
*/
- (void)groupEnterAndLeave {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"group---begin");
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_enter(group);
dispatch_async(queue, ^{
// 追加任务1
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
}
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(queue, ^{
// 追加任务2
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
}
dispatch_group_leave(group);
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 等前面的异步操作都执行完毕后,回到主线程.
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程
}
NSLog(@"group---end");
});
}
运行结果:
从dispatch_group_enter
、dispatch_group_leave
相关代码运行结果中可以看出:
- 当所有任务执行完成之后,才执行
dispatch_group_notify
中的任务。这里的dispatch_group_enter
、dispatch_group_leave
组合,其实等同于dispatch_group_async
。
dispatch_apply
通常我们会用 for 循环遍历,但是 GCD 给我们提供了快速迭代的函数dispatch_apply
。dispatch_apply
按照指定的次数将指定的任务追加到指定的队列中,并等待全部队列执行结束。
我们可以利用异步队列同时遍历。比如说遍历 0~5 这6个数字,for 循环的做法是每次取出一个元素,逐个遍历。dispatch_apply
可以同时遍历多个数字。
/*
* 快速迭代方法 dispatch_apply
*/
- (void)apply {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSLog(@"apply---begin");
dispatch_apply(6, queue, ^(size_t index) {
NSLog(@"%zd---%@",index, [NSThread currentThread]);
});
NSLog(@"apply---end");
}
运行结果:
从dispatch_apply相关代码执行结果中可以看出:
- 0~5 打印顺序不定,最后打印了
apply---end
。 - 因为是在并发队列中异步队执行任务,所以各个任务的执行时间长短不定,最后结束顺序也不定。但是
apply---end
一定在最后执行。这是因为dispatch_apply
函数会等待全部任务执行完毕。
dispatch_barrier_async
- GCD 栅栏方法:dispatch_barrier_async
我们有时需要异步执行两组操作,而且第一组操作执行完之后,才能开始执行第二组操作。这样我们就需要一个相当于 栅栏 一样的一个方法将两组异步执行的操作组给分割起来,当然这里的操作组里可以包含一个或多个任务。这就需要用到dispatch_barrier_async
方法在两个操作组间形成栅栏。
dispatch_barrier_async
函数会等待前边追加到并发队列中的任务全部执行完毕之后,再将指定的任务追加到该异步队列中。然后在dispatch_barrier_async
函数追加的任务执行完毕之后,异步队列才恢复为一般动作,接着追加任务到该异步队列并开始执行。具体如下图所示:
/**
* 栅栏方法 dispatch_barrier_async
*/
- (void)barrier {
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_barrier_async(queue, ^{
// 追加任务 barrier
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"barrier---%@",[NSThread currentThread]);// 打印当前线程
}
});
dispatch_async(queue, ^{
// 追加任务3
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程
}
});
dispatch_async(queue, ^{
// 追加任务4
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"4---%@",[NSThread currentThread]); // 打印当前线程
}
});
}
运行结果:
在dispatch_barrier_async
相关代码执行结果中可以看出:
- 在执行完栅栏前面的操作之后,才执行栅栏操作,最后再执行栅栏后边的操作。
dispatch_semaphore
- GCD 信号量:dispatch_semaphore
GCD 中的信号量是指 Dispatch Semaphore,是持有计数的信号。类似于过高速路收费站的栏杆。可以通过时,打开栏杆,不可以通过时,关闭栏杆。在 Dispatch Semaphore 中,使用计数来完成这个功能,计数为0时等待,不可通过。计数为1或大于1时,计数减1且不等待,可通过。Dispatch Semaphore 提供了三个函数。
dispatch_semaphore_create
:创建一个Semaphore并初始化信号的总量dispatch_semaphore_signal
:发送一个信号,让信号总量加1dispatch_semaphore_wait
:可以使总信号量减1,当信号总量为0时就会一直等待(阻塞所在线程),否则就可以正常执行。
注意:信号量的使用前提是:想清楚你需要处理哪个线程等待(阻塞),又要哪个线程继续执行,然后使用信号量。
Dispatch Semaphore 在实际开发中主要用于:
- 保持线程同步,将异步执行任务转换为同步执行任务
- 保证线程安全,为线程加锁
我们来思考一下这种情况:不考虑顺序,将所有数据追加到NSMutableArray
中。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSMutableArray *array = [[NSMutableArray alloc] init];
for (int i = 0; i < 100000; ++i) {
dispatch_async(queue, ^{
[array addObject:[NSNumber numberWithInt:i]];
});
}
因为该源代码使用Global Dispatch Queue更新NSMutbleArray
类对象,所以执行后由内存错误导致应用程序异常结束的概率很高。此时应使用Dispatch Semaphore。
- Dispatch Semaphore本来使用的是更细粒度的对象,不过本书还是使用该源代码对Dispatch Semphore进行说明。
- Dispatch Semaphore是持有技术的信号,该计数是多线程编程中的计数类型信号。所谓信号,类似于过马路时常用的手旗。而在Dispatch Semaphore中,使用计数来实现该功能。计数为0时等待,计数为1或大于1时,减去1而不等待。
什么是细粒度/粗粒度?
细粒度(fine-grained):粒度似乎根据项目模块划分的细致程度区分的,一个项目模块(或子模块)分得越多,每个模块(或子模块)越小,负责的工作越细,就说是细粒度。
粗粒度(coarse-grained):相对于细粒度而言,一个项目模块(或子模块)分得越少,每个模块(或子模块)越大,负责的工作越泛,就说是粗粒度。
粗粒度和细粒度是一个相对的概念,定义这个概念主要是出于重用的目的,比如:类的设计,为尽可能的重用则采用细粒度的设计模式,将一个复杂的类拆分成高度重用的细化的类。粗粒度和细粒度这个概念也出现在数据库的课本中,这里就不赘述了。
下面介绍一下使用方法。通过dispatch_semap_creat
函数生成Dispatch Semaphore。
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
- 参数表示计数的初始值。本例将计数值初始化为“1”。从函数名称中含有的
creat
可以看出,该函数与Dispatch Queue和Dispatch Group一样,必须通过dispatch_release
函数释放。当然,也可通过dispatch_retain
函数持有。
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_semphore_wait
函数等待Dispatch Semaphore的计数值达到大于或等于1。当计数值大于等于1,或者在待机中计数值大于等于1时,对该计数进行减法并从dispatch_semaphore_wait
函数返回。第二个参数与dispatch_group_wait
函数等相同,有dispatch_time_t
类型值指定等待时间。该例的参数意味着永久等待。另外,dispatch_group_wait
函数的返回值也与dispatch_group_wait
函数相同。可像以下源代码这样,通过返回值进行分支处理。
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 1ull*NSEC_PER_SEC);
long result = dispatch_semaphore_wait(semaphore, time);
if (result == 0) {
/*
* 由于dispatch Semaphore的计数达到大于等于1
* 或者在待机中的指定时间内
* Dispatch Semaphore的计数值达到大于等于1
* 所以Dispatch SEMAPHORE的计数值减去1
*
* 可执行需要进行排他控制的处理
*/
} else {
/*
* 由于Dispatch Semaphore的计数值为0
* 因此在达到指定时间为止待机
*/
}
dispatch_semaphore_wait
函数返回0时,可安全的执行需要进行排他控制的处理。该处理结束时通过dispatch_semaphore_signal
函数将Dispatch Semaphore的计数值加1.
我们在前面的源代码中实际使用Dispatch Semphore看看。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
/*
* 生成Dispatch Semaphore
* Dispatch Semaphore的计数初始值设定为“1”
*
* 保证可访问NSMutableArray类对象的线程
* 同时只能有1个
*/
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
NSMutableArray *array = [[NSMutableArray alloc] init];
for (int i = 0; i < 100000; ++i) {
dispatch_async(queue, ^{
/*
* 等待Dispatch Semaphore
* 一直等待,直到Dispatch Semaphore的计数值达到大于等于1
*
*/
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
/*
* 由于Dispatch Semphore的计数值达到大于等于1
* 所以将Dispatch Semaphore的计数值减去1,
* dispatch_semaphore_wait函数执行返回
*
* 即执行到此时时的
* Dispatch Semaphore的计数值恒为“0”
*
* 由于可访问NSMutableArray类对象的线程
* 只有1个
* 因此可安全地进行更新
*/
[array addObject:[NSNumber numberWithInt:i]];
/*
* 排他控制处理结束
* 所以通过dispatch_semaphore_signal函数
* 将Dispatch Semaphore的计数值增加的线程
*
* 如果有通过dispatch_semaphore_wait函数
* 等待Dispatch Semaphore的计数值增加的线程
* 就由最先等待的线程执行
*/
dispatch_semaphore_signal(semaphore);
});
}
/*
* 如果使用结束,需要如下选择
* 释放Dispatch Semaphore
*
* dispatch_release(semaphore)
*/
在没有Serial Dispatch Queue和dispatch_barrier_async
函数那么大粒度且一部分处理需要进行排他控制的情况下,Dispatch Semaphore便可发挥威力。