1 什么是GCD?
Grand Central Dispatch(GCD) 是 Apple 开发的一个多核编程的较新的解决方法。它主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。它是一个在线程池模式的基础上执行的并发任务。在 Mac OS X 10.6 雪豹中首次推出,也可在 iOS 4 及以上版本使用。
那么使用 GCD 有什么好处呢
- GCD可用与多核的并行运算
- GCD 会自动利用更多的CPU 内核
- GCD 会自动管理线程的生命周期
- 我们只需要告诉GCD 执行的任务,具体的线程管理就不需要管了
2 GCD 基本认识
其中比较重要的是两个概念 任务和队列
任务: 就是执行操作,换句话说就是在线程中执行的代码
其中执行任务有两种方式
同步执行
同步添加到任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里的任务执行完之后再继续执行
只能在当前线程执行任务,没有开启新线程的能力
异步执行
异步添加到任务队列中,不用等待,可以继续执行自己的任务
具备开启新线程的能力,可以在新线程中执行任务
队列: 是指等待队列也就是存放任务的队列。队列是一种特殊的线性表。先进先出
按照任务执行顺序的不同可以分为
- 串行队列
每次只能执行一个任务,一个挨着一个执行。 - 并发队列
可以多个任务同时执行
3 GCD 的具体使用
1 队列的创建
串行队列的创建
1 队列唯一标识符
2 队列类型
dispatch_queue_t queue = dispatch_queue_create("testQueue_one", DISPATCH_QUEUE_SERIAL);
并行队列的创建
dispatch_queue_t queue = dispatch_queue_create("testQueue_tow", DISPATCH_QUEUE_CONCURRENT);
GCD 的默认队列 即
主队列: 其实质是一个串行队列当前代码都会放在主队列里,即在主线程里执行
获取主队列
dispatch_queue_t = queue = dispathch_get_main_queue();
有默认的串行队列当然也有默认的并行队列 即 全局并行队列
1 表示队列优先级
2 暂时没用 可用0代替
dispatch_queue_t = queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
2 任务的创建
执行任务的方法也有两个 同步执行 和 异步执行
//同步执行创建
dispatch_sync(queue,^{
//这里写要执行的代码
})
//异步执行任务的创建方法
dispatch_async(queue,^{
//这里写要执行的代码
})
虽然GCD只有两种,但是和两种队列组合就有多种不同方式
- 同步执行 + 并发队列
- 异步执行 + 并发队列
- 同步执行 + 串行队列
- 异步执行 + 串行队列
如果加上系统的默认的主队列的话
就又多出两种
- 同步执行 + 主队列
- 异步执行 + 主队列
那么它们有什么区别呢?
3 具体使用
同步执行 + 并发队列
在当前线程中执行任务,不会开启新线程,执行完一个任务,再执行下一个任务
- (void)syncConcurrent {
NSLog(@"syncConcurrent---begin");
dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_sync(queue, ^{
// 追加任务 1
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_sync(queue, ^{
// 追加任务 2
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_sync(queue, ^{
// 追加任务 3
[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("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
// 追加任务 1
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_async(queue, ^{
// 追加任务 2
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_async(queue, ^{
// 追加任务 3
[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("net.bujige.testQueue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
// 追加任务 1
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_sync(queue, ^{
// 追加任务 2
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_sync(queue, ^{
// 追加任务 3
[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("net.bujige.testQueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
// 追加任务 1
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_async(queue, ^{
// 追加任务 2
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_async(queue, ^{
// 追加任务 3
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程
});
NSLog(@"asyncSerial---end");
}
- 开启了一条新线程(异步执行 具备开启新线程的能力,串行队列 只开启一个线程)。
- 所有任务是在打印的 syncConcurrent---begin 和 syncConcurrent---end 之后才开始执行的(异步执行 不会做任何等待,可以继续执行任务)。
- 任务是按顺序执行的(串行队列 每次只有一个任务被执行,任务一个接一个按顺序执行)。
下边讲讲刚才我们提到过的:主队列。
- 主队列:GCD 默认提供的 串行队列。
- 默认情况下,平常所写代码是直接放在主队列中的。
- 所有放在主队列中的任务,都会放到主线程中执行。
- 可使用 dispatch_get_main_queue() 获得主队列。
同步执行 + 主队列
同步执行 + 主队列 在不同的线程中国呢调用结果是不一样的,在主线程中调用就会发生死锁问题,而在其他线程中则不会
主线程执行 互相等待卡住不可行
- (void)syncMain {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"syncMain---begin");
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
// 追加任务 1
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_sync(queue, ^{
// 追加任务 2
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_sync(queue, ^{
// 追加任务 3
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程
});
NSLog(@"syncMain---end");
}
在主线程中使用 同步执行 + 主队列 可以惊奇的发现:
- 追加到主线程的任务 1、任务 2、任务 3 都不再执行了,而且 syncMain---end 也没有打印,在 XCode 9 及以上版本上还会直接报崩溃。这是为什么呢?
这是因为我们在主线程中执行 syncMain 方法,相当于把 syncMain 任务放到了主线程的队列中。而 同步执行 会等待当前队列中的任务执行完毕,才会接着执行。那么当我们把 任务 1 追加到主队列中,任务 1 就在等待主线程处理完 syncMain 任务。而syncMain 任务需要等待 任务 1 执行完毕,才能接着执行。
那么,现在的情况就是 syncMain 任务和 任务 1 都在等对方执行完毕。这样大家互相等待,所以就卡住了,所以我们的任务执行不了,而且 syncMain---end 也没有打印。
在其他线程中调用
不会开启新线程,执行完一个任务,再执行下一个任务
[NSThread detachNewThreadSelector:@selector(syncMain) toTarget:self withObject:nil];
在其他线程中使用 同步执行 + 主队列 可看到:
所有任务都是在主线程(非当前线程)中执行的,没有开启新的线程(所有放在主队列中的任务,都会放到主线程中执行)。
所有任务都在打印的 syncConcurrent---begin 和 syncConcurrent---end 之间执行(同步任务 需要等待队列的任务执行结束)。
任务是按顺序执行的(主队列是 串行队列,每次只有一个任务被执行,任务一个接一个按顺序执行)。
为什么现在就不会卡住了呢?
因为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
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_async(queue, ^{
// 追加任务 2
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_async(queue, ^{
// 追加任务 3
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程
});
NSLog(@"asyncMain---end");
}
在 异步执行 + 主队列 可以看到:
- 所有任务都是在当前线程(主线程)中执行的,并没有开启新的线程(虽然 异步执行 具备开启线程的能力,但因为是主队列,所以所有任务都在主线程中)。
- 所有任务是在打印的 syncConcurrent---begin 和 syncConcurrent---end 之后才开始执行的(异步执行不会做任何等待,可以继续执行任务)。
- 任务是按顺序执行的(因为主队列是 串行队列,每次只有一个任务被执行,任务一个接一个按顺序执行)。
4 GCD 线程间的通信
因为在开发过程中,我们的耗时操作都是在其他线程中完成的,但是在这些操作完成后是要回到主线程刷新ui的。这里就涉及到了线程之间的通信问题
(void)communication {
// 获取全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 获取主队列
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_async(queue, ^{
// 异步追加任务 1
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
// 回到主线程
dispatch_async(mainQueue, ^{
// 追加在主线程中执行的任务
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
});
});
可以看到在其他线程执行任务,执行结束后回到主线程执行相应操作
5 GCD 的其他方法
1 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
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_async(queue, ^{
// 追加任务 2
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_barrier_async(queue, ^{
// 追加任务 barrier
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"barrier---%@",[NSThread currentThread]);// 打印当前线程
});
dispatch_async(queue, ^{
// 追加任务 3
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_async(queue, ^{
// 追加任务 4
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"4---%@",[NSThread currentThread]); // 打印当前线程
});
}
可以看到 在执行完栅栏前面的操作之后,才执行的栅栏操作,最后再执行栅栏后的操作
2 dispatch_after 常用的延时方法
在指定时间(例如 3 秒)之后执行某个任务。可以用 GCD 的dispatch_after 方法来实现。
需要注意的是:dispatch_after 方法并不是在指定时间之后才开始执行处理,而是在指定时间之后将任务追加到主队列中。严格来说,这个时间并不是绝对准确的,但想要大致延迟执行任务,dispatch_after 方法是很有效的。
- (void)after {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"asyncMain---begin");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 2.0 秒后异步追加任务代码到主队列,并开始执行
NSLog(@"after---%@",[NSThread currentThread]); // 打印当前线程
});
}
可以看到 dispatch_after 里面的操作打印是其外操晚打印大约两秒
3 dispatch_once 一次性代码(只执行一次)
在我们创建单例,或者有整个程序运行过程只执行一次的代码时,我们就用到了GCD 的 dispatch_once 方法。使用 dispatch_once 方法能保证某段代码在程序运行过程中只被执行一次,并且即使在多线程的环境下,dispatch_once 也可以保证线程安全。
static dispatch_once_t onceToken;
dispatch_once(&onceToken,^{
只执行一次的代码
})
4 dispatch_group 队列组
我们面试或者平时会遇到这样的需求:分别异步执行两个耗时操作,然后当两个任务执行完毕后再回调到主线程执行任务。这时我们可以用到 dispatch_group
- 首先创建一个队列组 dispatch_group_create()
- 调用队列组的 dispatch_group_async 先把队列任务放到队列中,然后将队列放入到队列组中。或者使用队列组 dispatch_group_enter ,dispatch_group_leave组合来实现 dispatch_group_async。
- 调用队列的 dispathch_group_notify 回到指定线程执行任务。或者使用 dispatch_group_wait 回到当前线程继续向下执行(会阻塞当前线程)
dispatch_group_notify 监听group 中任务的完成状态,当所有的任务都执行完成后。追加任务到group中,并执行任务。
- (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
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 追加任务 2
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 等前面的异步任务 1、任务 2 都执行完毕后,回到主线程执行下边任务
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"group---end");
});
}
从上可以看出 当所有任务都执行完成之后,才执行 dispatch_group_notify 相关 block 中的任务。
dispatch_group_wait 暂停当前线程(阻塞当前线程)。等待指定的group中的任务完成后,才会往下继续执行
- (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
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 追加任务 2
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
});
// 等待上面的任务全部完成后,会往下继续执行(会阻塞当前线程)
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"group---end");
}
dispatch_group_enter、dispatch_group_leave 常用方式
dispatch_group_enter 标志着一个任务追加到group,执行一次。相当于group中未执行完毕任务数+1
dispatch_group_leave 标志着一个任务离开了 group,执行一次,相当于group中未执行完毕数-1
当goup中未执行完毕任务数为0的时候,才会使用dispatch_wait 解除阻塞,以及执行追加到 dispatch_group_notify 中的任务。
- (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
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(queue, ^{
// 追加任务 2
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
dispatch_group_leave(group);
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 等前面的异步操作都执行完毕后,回到主线程.
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"group---end");
});
}
可以看出 dispatch_group_enter、dispatch_group_leave 组合,其实等同于dispatch_group_async。