文章目录
一、GCD简介
1. GCD概念
Grand Central Dispatch(GCD)是苹果封装的一套C的API,异步执行任务的技术之一。它可以自动管理线程的创建,调度和销毁等功能,无需开发者自己实现。
苹果官方对GCD的说明:开发者只需要定义想执行的任务并追加到适当的Dispatch Queue中,GCD就能生成必要的线程并计划执行任务。
- 用于管理追加的Block的C语言层实现的FIFO队列
- Atomic函数中实现的用于排他控制的轻量级信号
- 用于管理线程的C语言层实现的一些容器
2. GCD的优点
- GCD可用于多核的并行运算
- GCD会自动利用更多的CPU内核(比如双核、四核)
- GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
- 程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码
二、GCD任务和队列
任务:就是执行操作的意思,换句话说就是你在线程中执行的那段代码。在GCD中是放在block中的,执行任务有两种方式:同步执行(sync) 和 异步执行(async)。两者的主要区别是:是否等待队列的任务执行结束,以及是否具备开启新线程的能力。
- 同步执行(sync):
- 同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。
- 只能在当前线程中执行任务,不具备开启新线程的能力。
- 异步执行(async):
- 异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。
- 可以在新的线程中执行任务,具备开启新线程的能力。
举个简单例子:你要打电话给小明和小白。
同步执行就是,你打电话给小明的时候,不能同时打给小白,等到给小明打完了,才能打给小白(等待任务执行结束)。而且只能用当前的电话(不具备开启新线程的能力)。
而异步执行就是,你打电话给小明的时候,不等和小明通话结束,还能直接给小白打电话,不用等着和小明通话结束再打(不用等待任务执行结束)。除了当前电话,你还可以使用其他所能使用的电话(具备开启新线程的能力)。
注意: 异步执行(async) 虽然具有开启新线程的能力,但是并不一定开启新线程。这跟任务所指定的队列类型有关。
队列(Dispatch Queue):这里的队列指执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的线性表,采用FIFO(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务。队列的结构可参考下图:
在GCD中有两种队列:串行队列 和 并发队列。两者都符合FIFO(先进先出)的原则。两者的主要区别是:执行顺序不同,以及开启线程数不同。
- 串行队列(Serial Dispatch Queue):
- 每次只有一个任务被执行。让任务一个接着一个地执行。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)
- 并发队列(Concurrent Dispatch Queue):
- 可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务)
注意:并发队列的并发功能只有在异步(dispatch_async)函数下才有效
两者具体区别如下两图所示:
三、GCD使用步骤
1. 创建方法
- 创建一个队列(串行队列或并发队列)
- 将任务追加到任务的等待队列中,然后系统就会根据任务类型执行任务(同步执行或异步执行)
创建方法 DispatchQueue.init(label: String, qos: DispatchQoS = .unspecified, attributes: DispatchQueue.Attributes = [], autoreleaseFrequency: DispatchQueue.AutoreleaseFrequency = .inherit, target: DispatchQueue? = nil)
;
label
队列的标识符,方便调试qos
用于指定队列的优先级attributes
队列的属性autoreleaseFrequency
自动释放频率。有些队列是会在履行完任务后自动释放的,有些比如Timer等是不会自动释放的,是需要手动释放。- 串行队列创建
// 串行队列创建
let queue = DispatchQueue(label: "test.queue")
指定队列优先级
let queue = DispatchQueue(label: "test.queue", qos: .userInitiated)
.userInteractive //用户交互(跟主线程一样)
.userInitiated //用户期望优先级(不要放太耗时的操作)
.default //默认的
.utility //公共的
.background //后台
.unspecified //不指定
优先级, 由上往下依次降低
- 并行队列创建
let queue = DispatchQueue(label: "test.queue", qos: .userInitiated, attributes: .concurrent)
-
对于串行队列,GCD提供了一种特殊的串行队列:主队列(Main Dispatch Queue).
- 所有放置主队列中的任务,都会放到主线程中执行。
- 可使用
DispatchQueue.main
获得主队列
-
对于并发队列,GCD默认提供了全局并发队列(Global Dispatch Queue)
- 可以使用
DispatchQueue.global()
来获取。
- 可以使用
2. GCD的基本使用
1. 并发队列 + 同步执行
在当前线程中执行任务,不会开启新线程,执行完一个任务,在执行下一个任务
private func syncConCurrent() {
print("syncConCurrent --- begin")
let queue = DispatchQueue(label: "test.queue", attributes: .concurrent)
queue.sync {
for _ in 0..<2 {
print("1------\(Thread.current)")
}
}
queue.sync {
for _ in 0..<2 {
print("2------\(Thread.current)")
}
}
queue.sync {
for _ in 0..<2 {
print("3------\(Thread.current)")
}
}
}
输出结果:
syncConCurrent — begin
1------<_NSMainThread: 0x6000017008c0>{number = 1, name = main}
1------<_NSMainThread: 0x6000017008c0>{number = 1, name = main}
2------<_NSMainThread: 0x6000017008c0>{number = 1, name = main}
2------<_NSMainThread: 0x6000017008c0>{number = 1, name = main}
3------<_NSMainThread: 0x6000017008c0>{number = 1, name = main}
3------<_NSMainThread: 0x6000017008c0>{number = 1, name = main}
说明:
- 所有任务都是在主线程中执行的(
同步执行
不具备开辟新线程的能力)。 - 由于只有一个线程,所以任务只能一个一个执行(
同步任务
需要等待队列的任务执行结束)。
2. 并发队列 + 异步执行
可以开启多个线程,任务交替(同时)执行。
private func asyncConcurrent() {
print("asyncConcurrent --- begin")
let queue = DispatchQueue(label: "test.asyncqueue", attributes: .concurrent)
queue.async {
for _ in 0..<2 {
print("1------\(Thread.current)")
}
}
queue.async {
for _ in 0..<2 {
print("2------\(Thread.current)")
}
}
queue.async {
for _ in 0..<2 {
print("3------\(Thread.current)")
}
}
print("asyncConcurrent---end")
}
输出结果:
syncConCurrent — begin
asyncConcurrent—end
1------<NSThread: 0x600001799780>{number = 9, name = (null)}
2------<NSThread: 0x60000174ce00>{number = 8, name = (null)}
2------<NSThread: 0x60000174ce00>{number = 8, name = (null)}
3------<NSThread: 0x600001704940>{number = 10, name = (null)}
1------<NSThread: 0x600001799780>{number = 9, name = (null)}
3------<NSThread: 0x600001704940>{number = 10, name = (null)}
说明
- 除了主线程,又开启了3个线程,并且任务是交替着同时执行的(
异步执行
具备开辟新线程的能力。并发队列
可开辟多个线程,同时执行多个任务)。 - 所有任务是在打印的syncConcurrent—begin和syncConcurrent—end之后才开始执行的。说明任务不是马上执行,而是将所有任务添加到队列之后才开始异步执行(
异步执行
不做等待,可以继续执行任务)。
3. 串行队列 + 同步执行
不会开启新线程,在当前线程执行任务。任务是串行的,执行完一个任务,再执行下一个任务。
private func syncSerial() {
print("syncSerial --- begin")
let queue = DispatchQueue(label: "test.syncSerial")
queue.sync {
for _ in 0..<2 {
print("1------\(Thread.current)")
}
}
queue.sync {
for _ in 0..<2 {
print("2------\(Thread.current)")
}
}
queue.sync {
for _ in 0..<2 {
print("3------\(Thread.current)")
}
}
print("syncSerial---end")
}
输出结果
syncSerial — begin
1------<_NSMainThread: 0x6000017048c0>{number = 1, name = main}
1------<_NSMainThread: 0x6000017048c0>{number = 1, name = main}
2------<_NSMainThread: 0x6000017048c0>{number = 1, name = main}
2------<_NSMainThread: 0x6000017048c0>{number = 1, name = main}
3------<_NSMainThread: 0x6000017048c0>{number = 1, name = main}
3------<_NSMainThread: 0x6000017048c0>{number = 1, name = main}
syncSerial—end
说明:
- 所有任务都是在主线程中执行的,并没有开启新的线程。
- 由于串行队列,所以按顺序一个一个执行
4. 串行队列 + 异步执行
可以开启多个线程,任务交替(同时)执行。
private func asyncSerial() {
print("asyncSerial --- begin")
let queue = DispatchQueue(label: "test.asyncSerial")
queue.async {
for _ in 0..<2 {
print("1------\(Thread.current)")
}
}
queue.async {
for _ in 0..<2 {
print("2------\(Thread.current)")
}
}
queue.async {
for _ in 0..<2 {
print("3------\(Thread.current)")
}
}
print("asyncSerial---end")
}
输出结果
asyncSerial — begin
asyncSerial—end
1------<NSThread: 0x60000174c700>{number = 8, name = (null)}
1------<NSThread: 0x60000174c700>{number = 8, name = (null)}
2------<NSThread: 0x60000174c700>{number = 8, name = (null)}
2------<NSThread: 0x60000174c700>{number = 8, name = (null)}
3------<NSThread: 0x60000174c700>{number = 8, name = (null)}
3------<NSThread: 0x60000174c700>{number = 8, name = (null)}
说明
- 开启了一条新线程,但是任务还是串行,所以任务是一个一个执行。
- 所有任务是在打印的syncConcurrent—begin和syncConcurrent—end之后才开始执行的。说明任务不是马上执行,而是将所有任务添加到队列之后才开始同步执行
5. 主队列 + 同步执行
同步执行 + 主队列在不同线程中调用结果也是不一样,在主线程中调用会出现死锁,而在其他线程中则不会。
private func syncMain() {
print("syncMain --- begin")
let queue = DispatchQueue.main
queue.sync {
for _ in 0..<2 {
print("1------\(Thread.current)")
}
}
queue.sync {
for _ in 0..<2 {
print("2------\(Thread.current)")
}
}
print("syncMain---end")
}
互相等待崩溃
说明:
- 在主线程中使用同步执行 + 主队列,追加到主线程的任务1、任务2、任务3都不再执行了,而且syncMain—end也没有打印,在XCode上还会报崩溃。
在主线程中执行syncMain方法,相当于把syncMain任务放到了主线程的队列中。而同步执行会等待当前队列中的任务执行完毕,才会接着执行。那么当我们把任务1追加到主队列中,任务1就在等待主线程处理完syncMain任务。而syncMain任务需要等待任务1执行完毕,才能接着执行。
那么,现在的情况就是syncMain任务和任务1都在等对方执行完毕。这样大家互相等待,所以就卡住了,所以我们的任务执行不了。 - 在其他线程中使用同步执行 + 主队列:
syncMain 任务放到了其他线程里,而任务1、任务2、任务3都在追加到主队列中,这三个任务都会在主线程中执行。syncMain 任务在其他线程中执行到追加任务1到主队列中,因为主队列现在没有正在执行的任务,所以,会直接执行主队列的任务1,等任务1执行完毕,再接着执行任务2、任务3。所以这里不会卡住线程。
6. 主队列 + 异步执行
只在主线程中执行任务,执行完一个任务,再执行下一个任务。
private func asyncMain() {
print("asyncMain --- begin")
let queue = DispatchQueue.main
queue.async {
for _ in 0..<2 {
print("1------\(Thread.current)")
}
}
queue.async {
for _ in 0..<2 {
print("2------\(Thread.current)")
}
}
print("asyncMain---end")
}
输出结果
syncMain — begin
syncMain—end
1------<_NSMainThread: 0x6000017048c0>{number = 1, name = main}
1------<_NSMainThread: 0x6000017048c0>{number = 1, name = main}
2------<_NSMainThread: 0x6000017048c0>{number = 1, name = main}
2------<_NSMainThread: 0x6000017048c0>{number = 1, name = main}
说明:
- 所有任务都是在当前线程(主线程)中执行的,并没有开启新的线程(虽然异步执行具备开启线程的能力,但因为是主队列,所以所有任务都在主线程中)。
- 所有任务是在打印的syncConcurrent—begin和syncConcurrent—end之后才开始执行的(异步执行不会做任何等待,可以继续执行任务)。
- 任务是按顺序执行的(因为主队列是串行队列,每次只有一个任务被执行,任务一个接一个按顺序执行)。
7. 全局队列+ 异步执行
private func asyncGloba() {
let queue = DispatchQueue.global()
for i in 0..<10 {
queue.async {
print("asyncGloba------\(Thread.current) \(i)")
}
}
}
输出结果
asyncGloba------<NSThread: 0x600001704480>{number = 4, name = (null)} 6
asyncGloba------<NSThread: 0x60000177b040>{number = 11, name = (null)} 8
asyncGloba------<NSThread: 0x600001704480>{number = 4, name = (null)} 9
asyncGloba------<NSThread: 0x600001791f00>{number = 9, name = (null)} 4
asyncGloba------<NSThread: 0x60000174a180>{number = 5, name = (null)} 1
asyncGloba------<NSThread: 0x60000174ab40>{number = 3, name = (null)} 0
asyncGloba------<NSThread: 0x60000177ad80>{number = 8, name = (null)} 2
asyncGloba------<NSThread: 0x600001789600>{number = 7, name = (null)} 3
asyncGloba------<NSThread: 0x600001788bc0>{number = 10, name = (null)} 7
asyncGloba------<NSThread: 0x60000177aa80>{number = 6, name = (null)} 5
说明:
- 说明是异步执行,没有马上执行,并且有开子线程执行
四、GCD的其他方法
1. GCD栅栏方法:dispatch_barrier_async
- 在执行完栅栏前面的操作之后,才执行栅栏操作,最后再执行栅栏后边的操作。
2. GCD延时执行方法:DispatchQueue.main.asyncAfter
asyncAfter
函数并不是在指定时间之后才开始执行处理,而是在指定时间之后将任务追加到主队列中。严格来说,这个时间并不是绝对准确的,但想要大致延迟执行任务,asyncAfter
函数是很有效的。
3. GCD一次性代码(只执行一次):dispatch_once
- 在swift中已经废弃了,swift中单例
public static let shared = ClassName()
- 我们在创建单例、或者有整个程序运行过程中只执行一次的代码时,我们就用到了 GCD 的
dispatch_once
函数。使用
dispatch_once 函数能保证某段代码在程序运行过程中只被执行1次,并且即使在多线程的环境下,dispatch_once
也可以保证线程安全。
4. GCD队列的循环、挂起、恢复
1. dispatch_apply
- 通常我们会用 for 循环遍历,但是 GCD 给我们提供了快速迭代的函数dispatch_apply。按照dispatch_apply指定的次数将指定的任务追加到指定的队列中,并等待全部队列执行结束。
- 在swift使用下面方法
public class func concurrentPerform(iterations: Int, execute work: (Int) -> Swift.Void)
本来循环执行就是为了节约时间的嘛,所以默认就是用了并行队列。
private func dispatchApply() {
print("DispatchApply Begin")
DispatchQueue.concurrentPerform(iterations: 10) { (index) in
print("times:\(index)---- \(Thread.current)")
}
print("DispatchApply End")
}
结果:
DispatchApply Begin
times:5---- <NSThread: 0x600001710640>{number = 11, name = (null)}
times:6---- <NSThread: 0x60000170c040>{number = 6, name = (null)}
times:1---- <NSThread: 0x600001793940>{number = 8, name = (null)}
times:0---- <_NSMainThread: 0x6000017040c0>{number = 1, name = main}
times:4---- <NSThread: 0x600001751300>{number = 7, name = (null)}
times:7---- <NSThread: 0x600001727580>{number = 5, name = (null)}
times:2---- <NSThread: 0x600001750840>{number = 10, name = (null)}
times:8---- <NSThread: 0x600001793940>{number = 8, name = (null)}
times:9---- <_NSMainThread: 0x6000017040c0>{number = 1, name = main}
times:3---- <NSThread: 0x60000170cc40>{number = 9, name = (null)}
DispatchApply End
- 所有任务都是并行执行的,循环执行并行队列中的任务时,会开辟新的线程,也有可能会在当前线程中执行一些任务。
2. 队列的挂起与唤醒
挂起是不会暂停正在执行的队列的哈,只能是挂起还没执行的队列。
private func dispatchSuspend() {
let queue = DispatchQueue(label: "new thread")
// 挂起
queue.suspend()
queue.async {
print("The queue is suspended. Now it has completed.\n The queue is \"\(queue.label)\". ")
}
print("The thread will sleep for 3 seconds' time")
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.3) {
// 唤醒,开始执行
queue.resume()
}
}
结果:
The thread will sleep for 3 seconds’ time
The queue is suspended. Now it has completed.
The queue is “new thread”.
5. GCD队列组:DispatchGroup
有时候我们会有这样的需求:分别异步执行2个耗时任务,然后当2个耗时任务都执行完毕后再回到主线程执行任务。这时候我们可以用到 GCD 的队列组。
5.1 DispatchGroup.notify()
监听 group 中任务的完成状态,当所有的任务都执行完成后,追加任务到 group 中,并执行任务。
5.2 DispatchGroup.wait()
暂停当前线程(阻塞当前线程),等待指定的 group 中的任务执行完成后,才会往下继续执行。
5.3 DispatchGroup.enter()、DispatchGroup.leave()
enter()
标志着一个任务追加到 group,执行一次,相当于 group 中未执行完毕任务数+1leave()
标志着一个任务离开了 group,执行一次,相当于 group 中未执行完毕任务数-1。- 当 group 中未执行完毕任务数为0的时候,才会使
wait()
解除阻塞,以及执行追加到notify()
中的任务。
6. GCD信号量:DispatchSemaphore
GCD 中的信号量是指 Dispatch Semaphore,是持有计数的信号。类似于过高速路收费站的栏杆。可以通过时,打开栏杆,不可以通过时,关闭栏杆。在 Dispatch Semaphore 中,使用计数来完成这个功能,计数为0时等待,不可通过。计数为1或大于1时,计数减1且不等待,可通过。
Dispatch Semaphore 三个函数:
DispatchSemaphore.init(value: 1)
:创建一个Semaphore并初始化信号的总量DispatchSemaphore.signal()
:发送一个信号,让信号量加1DispatchSemaphore.wait()
:可以使总信号量减1,当信号量为0时就会一直等待(阻塞所在线程),否则就可以正常执行。
注意:信号量的使用前提是:想清楚你需要处理哪个线程等待(阻塞),又要哪个线程继续执行,然后使用信号量。
Dispatch Semaphore 在实际开发中主要用于:
- 保持线程同步,将异步执行任务转换为同步执行任务
- 保证线程安全,为线程加锁
六、GCD线程间的通信
在iOS开发过程中,一般在主线程里边进行UI刷新。我们通常把一些耗时的操作放在其他线程,比如说图片下载、文件上传等耗时操作。而当我们有时候在其他线程完成了耗时操作时,需要回到主线程,那么就用到了线程之间的通讯。
private func communicatioin() {
// 获取全局并发队列
let queue = DispatchQueue.global()
// 获取主队列
let mainQueue = DispatchQueue.main
queue.async {
// 异步追加任务
for _ in 0..<2 {
Thread.sleep(forTimeInterval: 2)
print("1---\(Thread.current)")
}
// 回到主线程
mainQueue.async {
// 追加在主线程中执行的任务
Thread.sleep(forTimeInterval: 2)
print("2---\(Thread.current)")
}
}
}
输出结果:
1—<NSThread: 0x600001754c40>{number = 8, name = (null)}
1—<NSThread: 0x600001754c40>{number = 8, name = (null)}
2—<_NSMainThread: 0x600001704a40>{number = 1, name = main}
说明:
- 在其他线程中先执行任务,执行完了之后回到主线程执行主线程的相应操作。
文章出处:iOS多线程:『GCD』详尽总结