赞
踩
全称是Grand Central Dispatch,底层是纯c语言,GCD 的核心就是为了解决如何让程序有序、高效的运行
GCD的优势:
GCD是苹果公司为多核的并行运算提出的解决方案
GCD会自动利用更多的CPU内核(比如双核、四核)
GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码
前导知识:
进程
是系统中正在运行的一个程序,程序一旦运行就是进程
。
线程
是进程
中执行运算的最小单位,负责当前进程中程序的执行。
一个进程
至少有一个线程
,一个进程
可以运行多个线程
,同一进程
的多个线程
可共享数据。
进程
是操作系统资源分配的基本单位,而线程
是处理器任务调度和执行的基本单位。
**任务:**一段代码、一个API调用、一个方法、函数、闭包等,一个应用就是由很多任务组成。
任务执行时间: 任务执行时间与线程状态、CPU调度、线程池调度、队列的优先级、任务的复杂度有关。
**队列(排队处理的任务):**FIFO(先进先出)先排队的先受理。
队列是用于保存以及管理任务的,线程
负责去队列中取任务进行执行。也可以理解为队列调度任务给到线程中执行。
1.串行队列:
串行队列(DISPATCH_QUEUE_SERIAL) : 每次只有一个任务被执行。让任务一个接着一个地执行。一般只开启一个线程,一个任务执行完毕后,再执行下一个任务(特例在后面)。
重要特征:串行队列中执行任务不允许被当前队列中的任务阻塞(此时会死锁),但可以被别的队列任务阻塞。
创建串行队列:
let serialQueue = DispatchQueue(label: "com.xxx.xxx.queueName")
2.并发队列:
并发队列(DISPATCH_QUEUE_CONCURRENT) : 放到并发队列的任务,GCD也会 FIFO的取出来,放在多个线程中执行,看起来,所有的任务都是一起执行的。
**重要特征:**系统会为并行队列至少分配一个线程,队列允许被任何队列的任务阻塞。
创建并发队列:
let concurrent = DispatchQueue(label: "com.xxx.xxx.queueName", attributes: .concurrent)
在以上操作的手动创建队列之前,系统就已经默认建好了6条队列,1条系统主队列(串行),5条全局并发队列(不同优先级),它们是我们创建的所有队列的最终目标队列,这6个队列负责所有队列的线程调度。
3.系统主队列:
系统主队列是一个串行队列,它主要处理UI相关任务和少量不耗时间和资源的操作,并且在主函数调用前生成,静态创建,UI只能在主线程更新。
(类属性)获取主队列:
let mainQueue = DispatchQueue.main
4.系统全局并发队列:
全局并发队列:存在5个不同的QoS级别,可以使用默认优先级,也可以单独指定,全局队列底层由数组创建,平时使用网络请求(例如第三方包Alamofire)都是对全局并发队列进行了一个封装,所以看不到直接使用的代码。
获取全局并发队列:
let globalQueue = DispatchQueue.global() // qos: .default
let globalQueue = DispatchQueue.global(qos: .background) // 后台运行级别
全局并发队列的一个应用
此时需要在页面上放置一个网络图片,可以先用并发全局队列获取该图片(防止卡顿),再回到主队列渲染UI
DispatchQueue.global().async{
let data = try! Data(contentsOf: URL(string: "aaa.jpg")!)
let image = UIImage(data)!
DispatchQueue.main.async{
self.imageView.image = image
}
}
假设当前在主队列中执行,此时存在一个手动创建的串行队列serialQueue和一个并发队列concurrentQueue。
1.serialQueue队列中没有任务在执行,那么提交一个同步任务在serialQueue队列,此时主队列阻塞,同步任务进入serialQueue队列执行,同步队列执行完后返回主队列执行,没有开辟新线程,是通过队列调度线程在两个队列中切换执行。
let serialQueue = DispatchQueue(label: ".com1")
serialQueue.sync {
print("同步1",Thread.current)
}
print("同步2", Thread.current)
输出结果:
同步1 <_NSMainThread: 0x600002770480>{number = 1, name = main}
同步2 <_NSMainThread: 0x600002770480>{number = 1, name = main}
2.主队列中加入一个主队列同步任务,或者在serialQueue队列执行过程中再加入一个同步任务,这时会发生死锁,可以这样理解,一个串行队列中有一个代码块任务在执行,代码块中有一行代码要在当前队列加入一个同步任务,由于是串行队列,任务是按顺序一个一个执行的,同步任务想要执行就必须等待代码块任务执行完,而代码块任务又被要求等待同步任务执行完才能继续执行,此时就形成了一个互相等待的局面,造成死锁。
主队列死锁:
DispatchQueue.main.sync{
print("同步", Thread.current)
}
报错:
serialQueue死锁:
let serialQueue = DispatchQueue(label: ".com1")
serialQueue.sync {
print("同步1",Thread.current)
serialQueue.sync {
print("同步2",Thread.current)
}
}
报错:
3.并发队列中有任务在执行,此时加入一个同步任务,线程会转去执行该同步任务,结束后再回到原任务,可以把并行队列想象成很多行串行队列组成的队列,加入同步任务时,线程离开正在执行的一行转而去同步任务添加的那一行执行,此时没有创建新线程,所以一直是并发队列调度一个线程执行不同行的任务。
let concurrentQueue = DispatchQueue(label: ".com2", attributes: .concurrent)
concurrentQueue.sync {
print("同步1", Thread.current)
concurrentQueue.sync {
print("同步2", Thread.current)
}
print("同步3",Thread.current)
}
输出结果:
同步1 <_NSMainThread: 0x600002710480>{number = 1, name = main}
同步2 <_NSMainThread: 0x600002710480>{number = 1, name = main}
同步3 <_NSMainThread: 0x600002710480>{number = 1, name = main}
测试任务是否在指定队列中,通过给队列一个标识,使用DispatchQueue.getSpecific方法来获取当前队列的标识,如果能获取到,说明任务在队列中。
//队列类型 enum DispatchTaskType: String{ case serial case concurrent case main case global } //定义队列 let serialQueue = DispatchQueue(label: "com.serialQueue") let concurrentQueue = DispatchQueue(label: "com.concurrentQueue", attributes: .concurrent) let mainQueue = DispatchQueue.main let globalQueue = DispatchQueue.global() //定义队列key let serialQueueKey = DispatchSpecificKey<String>() let concurrentQueueKey = DispatchSpecificKey<String>() let mainQueueKey = DispatchSpecificKey<String>() let globalQueueKey = DispatchSpecificKey<String>() //初始化队列 override func loadView() { super.loadView() serialQueue.setSpecific(key: serialQueueKey, value: DispatchTaskType.serial.rawValue) concurrentQueue.setSpecific(key: concurrentQueueKey, value: DispatchTaskType.concurrent.rawValue) mainQueue.setSpecific(key: mainQueueKey, value: DispatchTaskType.main.rawValue) globalQueue.setSpecific(key: globalQueueKey, value: DispatchTaskType.global.rawValue) } func testIsTaskInQueue(_ queueType: DispatchTaskType, key: DispatchSpecificKey<String>){ let value = DispatchQueue.getSpecific(key: key) let opnValue: String? = queueType.rawValue print("Is task in \(queueType.rawValue) queue: \(value == opnValue)") } override func viewDidLoad() { super.viewDidLoad() serialQueue.sync { self.testIsTaskInQueue(.serial, key: serialQueueKey) } }
输出结果:
Is task in serial queue: true
1.并行队列中新增异步任务:此时会新开一个线程,任务同时执行在不同线程上。
concurrentQueue.async {
print("异步1", Thread.current)
}
执行结果:
异步1 <NSThread: 0x6000018d41c0>{number = 7, name = (null)}
2.串行队列中新增异步任务:此时新开一个线程,串行队列的异步任务执行在新线程上
serialQueue.async {
print("异步1", Thread.current)
}
执行结果:
异步1 <NSThread: 0x6000018d41c0>{number = 6, name = (null)}
3.串行队列任务中嵌套本队列的异步任务:先同步阻塞了主队列,在主线程中执行同步任务,执行到新增异步任务语句开辟一个新线程,但由于串行队列任务只能一个接一个执行,所以即使此刻有一个新线程,异步任务仍然要添加在串行队列的队尾,直到同步任务执行结束,该异步任务才通过新线程执行,注意此时若主队列也有新任务,两个串行队列的执行互不影响(不同线程),类似于并发队列的不同行通过不同线程执行。
//两个串行队列没有固定顺序
let serialQueue = DispatchQueue(label: ".com1")
serialQueue.sync {
print("同步1",Thread.current)
serialQueue.async {
print("异步1",Thread.current)
}
print("同步2",Thread.current)
}
print("同步3", Thread.current)//这里后续的任务和serialQueue的异步任务互不影响
输出结果:
同步1 <_NSMainThread: 0x600003ebc000>{number = 1, name = main}
同步2 <_NSMainThread: 0x600003ebc000>{number = 1, name = main}
同步3 <_NSMainThread: 0x600003ebc000>{number = 1, name = main}
异步1 <NSThread: 0x600003ea1a00>{number = 7, name = (null)}
若在主队列后续任务前加一个延时:
//两个串行队列没有固定顺序
let serialQueue = DispatchQueue(label: ".com1")
serialQueue.sync {
print("同步1",Thread.current)
serialQueue.async {
print("异步1",Thread.current)
}
print("同步2",Thread.current)
}
Thread.sleep(until: .now + 0.2)
print("同步3", Thread.current)
输出结果:
同步1 <_NSMainThread: 0x600001ea8380>{number = 1, name = main}
同步2 <_NSMainThread: 0x600001ea8380>{number = 1, name = main}
异步1 <NSThread: 0x600001eada80>{number = 5, name = (null)}
同步3 <_NSMainThread: 0x600001ea8380>{number = 1, name = main}
分类
sync + DISPATCH_QUEUE_SERIAL : 阻塞当前线程取出的任务一个一个执行 所以不会创建线程
sync + DISPATCH_QUEUE_CONCURRENT : 因为会阻塞当前线程 所以即使是并发队列 一样是一个一个任务执行 不会创建线程
async + DISPATCH_QUEUE_SERIAL : 不会阻塞当前的线程 但是任务是一个一个取出来执行的 所以会创建一个线程
async + DISPATCH_QUEUE_CONCURRENT : 不会阻塞当前线程 任务取出来放到其他线程中 所以会创建很多线程 由系统控制
默认代码为串行同步,网络请求为并发异步,这两个组合为常用组合
栅栏任务的主要特性是可以对队列中的任务进行阻隔,执行栅栏任务时,它会先等待队列中已有的任务全部执行完成,然后它再执行,在它之后加入的任务也必须等栅栏任务执行完后才能执行。
这个特性更适合并行队列,而且对栅栏任务使用同步或异步方法效果都相同。
创建方式,先创建 WorkItem
,标记为:barrier
,再添加至队列中:
let queue = DispatchQueue(label: "com.zhalan", attributes: .concurrent)
let task = DispatchWorkItem(flags: .barrier) {
print(Thread.current)
}
queue.async(execute: task)
queue.sync(execute: task)
输出结果:
<NSThread: 0x6000000ec780>{number = 7, name = (null)}
<_NSMainThread: 0x6000000bc700>{number = 1, name = main}
示例:
并行队列中执行栅栏任务
/// 栅栏任务 func barrierTask() { let queue = concurrentQueue let barrierTask = DispatchWorkItem(flags: .barrier) { print("栅栏任务", Thread.current) } queue.async { print("任务1", Thread.current) } queue.async { print("任务2", Thread.current) } queue.async { print("任务3", Thread.current) } queue.async(execute: barrierTask) // 栅栏任务 queue.async { print("任务4", Thread.current) } queue.async { print("任务5", Thread.current) } queue.async { print("任务6", Thread.current) } }
输出结果:
任务2 <NSThread: 0x600001262100>{number = 4, name = (null)}
任务1 <NSThread: 0x600001239780>{number = 6, name = (null)}
任务3 <NSThread: 0x600001271b40>{number = 5, name = (null)}
栅栏任务 <NSThread: 0x600001271b40>{number = 5, name = (null)}
任务4 <NSThread: 0x600001271b40>{number = 5, name = (null)}
任务6 <NSThread: 0x600001262100>{number = 4, name = (null)}
任务5 <NSThread: 0x600001239780>{number = 6, name = (null)}
栅栏任务上下的任务输出顺序不确定
并行队列利用多个线程执行任务,可以提高程序执行的效率。而迭代任务可以更高效地利用多核性能,它可以利用 CPU 当前所有可用线程进行计算(任务小也可能只用一个线程)。如果一个任务可以分解为多个相似但独立的子任务,那么迭代任务是提高性能最适合的选择。
使用 concurrentPerform
方法执行迭代任务,迭代任务的后续任务需要等待它执行完成才会继续。本方法类似于 Objc 中的 dispatch_apply
方法,创建方式如下:
DispatchQueue.concurrentPerform(iterations: 10) {(index) -> Void in // 10 为迭代次数,可修改。
// do something
}
迭代任务可以单独执行,也可以放在指定的队列中:
let queue = DispatchQueue.global() // 全局并发队列
queue.async {
DispatchQueue.concurrentPerform(iterations: 100) {(index) -> Void in
// do something
}
//可以转至主线程执行其他任务
DispatchQueue.main.async {
// do something
}
}
示例:
本示例查找 1-100 之间能被 13 整除的整数,我们直接使用 10000 次迭代对每个数进行判断,符合的通过异步方法写入到结果数组中:
/// 迭代任务 func concurrentPerformTask() { /// 判断一个数是否能被另一个数整除 func isDividedExactlyBy(_ divisor: Int, with number: Int) -> Bool { return number % divisor == 0 } let array = Array(1...100) var result: [Int] = [] globalQueue.async { //通过concurrentPerform,循环变量数组 print("迭代任务开始") DispatchQueue.concurrentPerform(iterations: 100) {(index) -> Void in if isDividedExactlyBy(13, with: array[index]) { print("find a match: \(array[index])", Thread.current) self.mainQueue.async { result.append(array[index]) } } } print("迭代任务结束") //执行完毕,主线程更新结果。 DispatchQueue.main.sync { print("回到主线程") print("result: 找到了 \(result.count) 个数字 - \(result)") } } }
迭代任务开始
find a match: 39 <NSThread: 0x600001f342c0>{number = 4, name = (null)}
find a match: 26 <NSThread: 0x600001f65c00>{number = 6, name = (null)}
find a match: 13 <NSThread: 0x600001f284c0>{number = 5, name = (null)}
find a match: 52 <NSThread: 0x600001f28340>{number = 8, name = (null)}
find a match: 78 <NSThread: 0x600001f2ee80>{number = 7, name = (null)}
find a match: 91 <NSThread: 0x600001f3c400>{number = 9, name = (null)}
find a match: 65 <NSThread: 0x600001f62580>{number = 3, name = (null)}
迭代任务结束
回到主线程
result: 找到了 7 个数字 - [39, 26, 13, 52, 78, 91, 65]
创建队列的完整方法如下:
public convenience init(label: String, qos: DispatchQoS = .unspecified, attributes: DispatchQueue.Attributes = [], autoreleaseFrequency: DispatchQueue.AutoreleaseFrequency = .inherit, target: DispatchQueue? = nil)
队列在执行上是有优先级的,更高的优先级可以享受更多的计算资源,从高到低包含以下几个等级:
包含两个属性:
queue.activate()
方法。这个属性表示 autorelease pool
的自动释放频率, autorelease pool
管理着任务对象的内存周期。
包含三个属性:
autorelease pool
,需要手动管理。一般任务采用 .workItem
属性就够了,特殊任务如在任务内部大量重复创建对象的操作可选择 .never
属性手动创建 autorelease pool
。
这个属性设置的是一个队列的目标队列,即实际将该队列的任务放入指定队列中运行。目标队列最终约束了队列优先级等属性。
在程序中手动创建的队列,其实最后都指向系统自带的 主队列
或 全局并发队列
。
手动创建队列的好处是可以将任务进行分组管理。如单独阻塞队列中的任务,而不是阻塞系统队列中的全部任务。如果阻塞了目标队列,所有指向它的原队列也将被阻塞。
在 Swift 3 及之后,对目标队列的设置进行了约束,只有两种情况可以显式地设置目标队列(原因参考):
attributes
设定为 initiallyInactive
,然后在队列执行 activate()
之前可以指定目标队列。在其他地方都不能再改变目标队列。
等待一段时间后再进入队列中,这时候可以使用 asyncAfter
方法.
class AsyncAfter {
/// 延迟执行闭包
static func dispatch_later(_ time: TimeInterval, block: @escaping ()->()) {
let t = DispatchTime.now() + time
DispatchQueue.main.asyncAfter(deadline: t, execute: block)
}
}
AsyncAfter.dispatch_later(2) {
print("打个电话 at: \(Date())") // 将在 2 秒后执行
}
示例:封装一个方法,可以延迟执行任务,在计时结束前还可以取消任务或者将原任务替换为一个新任务。主要的思路是,将延迟后实际执行的任务代码进行替换,替换为空闭包则相当于取消了任务,或者替换为你想执行的其他任务:
AfterTask.swift文件
class AsyncAfter { typealias ExchangableTask = (_ newDelayTime: TimeInterval?, _ anotherTask:@escaping (() -> ()) ) -> Void /// 延迟执行一个任务,并支持在实际执行前替换为新的任务,并设定新的延迟时间。 /// /// - Parameters: /// - time: 延迟时间 /// - yourTask: 要执行的任务 /// - Returns: 可替换原任务的闭包 static func delay(_ time: TimeInterval, yourTask: @escaping ()->()) -> ExchangableTask { var exchangingTask: (() -> ())? // 备用替代任务 var newDelayTime: TimeInterval? // 新的延迟时间 let finalClosure = { () -> Void in //最后会执行的闭包 if exchangingTask == nil { //如果没有传入新的任务(替换原任务的更改任务) DispatchQueue.main.async(execute: yourTask)//执行原任务 } else { if newDelayTime == nil {//如果需要执行新任务,且没有延迟,立刻执行 DispatchQueue.main.async { print("任务已更改,现在是:\(Date())") exchangingTask!()//执行新任务(exchangingTask在新任务调用时被赋值成新闭包了) } } print("原任务取消了,现在是:\(Date())") } } dispatch_later(time) { finalClosure() }//原任务经过原延迟时间后执行 let exchangableTask: ExchangableTask = //返回给用户的可添加新任务的闭包 { delayTime, anotherTask in //新的延迟时间和新任务 exchangingTask = anotherTask //赋值新任务 newDelayTime = delayTime //赋值新的延迟时间(可为nil,立刻执行) if delayTime != nil { //如果有新的延迟时间 self.dispatch_later(delayTime!) { //经过新的延迟时间后 anotherTask() //执行新任务 print("任务已更改,现在是:\(Date())") } } } return exchangableTask } }
ViewController.swift文件
override func viewDidLoad() {
super.viewDidLoad()
let newTask = AsyncAfter.delay(2) {
print("OldTask")
}
newTask(2) {//若闭包为空则表示为取消任务,newTask(nil){}(nil可以替换成别的时间,表示更改任务的时间)取消任务时间按原计划
print("NewTask")
}
}
输出结果:
NewTask
原任务取消了,现在是:2023-11-18 06:24:58 +0000
任务已更改,现在是:2023-11-18 06:24:58 +0000
GCD 提供了一套机制,可以挂起队列中尚未执行的任务,已经在执行的任务会继续执行完,后续还可以手动再唤醒队列。
挂起使用 suspend()
,唤醒使用 resume()
。对于队列,这两个方法调用时需配对,因为可以多次挂起,调用唤醒的次数应等于挂起的次数才能生效,唤醒的次数更多则会报错,所以使用时最好设置一个计数器,或者封装一个挂起、唤醒的方法,在方法内部进行检查。
而对于 DispatchSource
则有所不同,它必须先调用 resume()
才能接收消息,所以此时唤醒的数量等于挂起的数量加一。
示例:
// // SusResume.swift // test123 // // Created by 李跃行 on 2023/11/18. // import Foundation class CreateQueueWithTask{ let concurrentQueue = DispatchQueue(label: "com.concurrentQueue", attributes: .concurrent) func printCurrentThread(with: String){ print(with, Thread.current) } } /// 挂起、唤醒测试类 class SuspendAndResum { let createQueueWithTask = CreateQueueWithTask() var concurrentQueue: DispatchQueue { return createQueueWithTask.concurrentQueue } var suspendCount = 0 // 队列挂起的次数 // MARK: ---------队列方法------------ /// 挂起测试 func suspendQueue() { createQueueWithTask.printCurrentThread(with: "start test\n") concurrentQueue.async { self.createQueueWithTask.printCurrentThread(with: "concurrentQueue async task1\n") } concurrentQueue.async { self.createQueueWithTask.printCurrentThread(with: "concurrentQueue async task2\n") } ————————————————————————————————可替换区域—————————————————————————————————————————— // 通过栅栏挂起任务 let barrierTask = DispatchWorkItem(flags: .barrier) { self.safeSuspend(self.concurrentQueue) } concurrentQueue.async(execute: barrierTask) print(123) //通过同步挂起任务 concurrentQueue.sync { self.safeSuspend(self.concurrentQueue) } print(456) ———————————————————————————————————————————————————————————————————————————————————— concurrentQueue.async { self.createQueueWithTask.printCurrentThread(with: "concurrentQueue async task3\n") } concurrentQueue.async { self.createQueueWithTask.printCurrentThread(with: "concurrentQueue async task4\n") } concurrentQueue.async { self.createQueueWithTask.printCurrentThread(with: "concurrentQueue async task5\n") } createQueueWithTask.printCurrentThread(with: "end test") } /// 唤醒测试 func resumeQueue() { self.safeResume(self.concurrentQueue) } /// 安全的挂起操作 func safeSuspend(_ queue: DispatchQueue) { suspendCount += 1 queue.suspend() print("任务挂起了") } /// 安全的唤醒操作 func safeResume(_ queue: DispatchQueue) { if suspendCount == 1 { queue.resume() suspendCount = 0 print("任务唤醒了") } else if suspendCount < 1 { print("唤醒的次数过多") } else { queue.resume() suspendCount -= 1 print("唤醒的次数不够,还需要 \(suspendCount) 次唤醒。") } } }
调用代码:
@IBAction func resume(_ sender: UIButton) {
suspendAndResum.resumeQueue()
}
@IBAction func suspend(_ sender: UIButton) {
suspendAndResum.suspendQueue()
}
UI:
这里要注意可替换区域三种情况:
1.若直接挂起任务,连续点击2次挂起,会把每次点击产生的任务直接添加到concurrentQueue队列中,需要点击对应次数的唤醒,才能执行后续任务,可替换区域代码如下。
self.safeSuspend(self.concurrentQueue)
输出结果:
start test <_NSMainThread: 0x60000079c540>{number = 1, name = main} 任务挂起了 end test <_NSMainThread: 0x60000079c540>{number = 1, name = main} concurrentQueue async task1 <NSThread: 0x6000007d9100>{number = 6, name = (null)} concurrentQueue async task2 <NSThread: 0x6000007d5100>{number = 3, name = (null)} start test <_NSMainThread: 0x60000079c540>{number = 1, name = main} 任务挂起了 end test <_NSMainThread: 0x60000079c540>{number = 1, name = main} 唤醒的次数不够,还需要 1 次唤醒。 任务唤醒了 concurrentQueue async task3 <NSThread: 0x600000797300>{number = 4, name = (null)} concurrentQueue async task5 <NSThread: 0x6000007f7e40>{number = 9, name = (null)} concurrentQueue async task4 <NSThread: 0x6000007d1640>{number = 8, name = (null)} concurrentQueue async task1 <NSThread: 0x600000797300>{number = 4, name = (null)} concurrentQueue async task2 <NSThread: 0x6000007d1380>{number = 10, name = (null)} concurrentQueue async task3 <NSThread: 0x6000007f7e40>{number = 9, name = (null)} concurrentQueue async task4 <NSThread: 0x6000007d11c0>{number = 11, name = (null)} concurrentQueue async task5 <NSThread: 0x6000007f7dc0>{number = 12, name = (null)}
2.若使用栅栏任务来挂起任务,连续点击两次挂起,在第一次挂起时,栅栏任务所在队列的剩余任务就被挂起了,再次点击挂起,由于前面的任务还是挂起状态,所以此时新添加的任务(包括栅栏任务)也处于挂起状态,栅栏任务没有被第二次执行,所以唤醒只用点击一次即可执行队列中的任务,直到再次执行到队列中添加的栅栏任务,此时再次被挂起,可替换区域代码如下。
// 通过栅栏挂起任务
let barrierTask = DispatchWorkItem(flags: .barrier) {
self.safeSuspend(self.concurrentQueue)
}
concurrentQueue.async(execute: barrierTask)
运行结果:
start test <_NSMainThread: 0x600003c68480>{number = 1, name = main} concurrentQueue async task1 <NSThread: 0x600003c38280>{number = 3, name = (null)} end test <_NSMainThread: 0x600003c68480>{number = 1, name = main} concurrentQueue async task2 <NSThread: 0x600003c11440>{number = 8, name = (null)} 任务挂起了 start test <_NSMainThread: 0x600003c68480>{number = 1, name = main} end test <_NSMainThread: 0x600003c68480>{number = 1, name = main} 任务唤醒了 concurrentQueue async task3 <NSThread: 0x600003c11440>{number = 8, name = (null)} concurrentQueue async task4 <NSThread: 0x600003c0fe80>{number = 9, name = (null)} concurrentQueue async task5 <NSThread: 0x600003c38280>{number = 3, name = (null)} concurrentQueue async task1 <NSThread: 0x600003c0f600>{number = 10, name = (null)} concurrentQueue async task2 <NSThread: 0x600003c11440>{number = 8, name = (null)} 任务挂起了 任务唤醒了 concurrentQueue async task3 <NSThread: 0x600003c11440>{number = 8, name = (null)} concurrentQueue async task4 <NSThread: 0x600003c0f600>{number = 10, name = (null)} concurrentQueue async task5 <NSThread: 0x600003c38280>{number = 3, name = (null)}
3.若使用同步任务挂起任务,连续点击两次挂起,在第一次挂起时,同步任务所在队列的剩余任务就被挂起了,再次点击挂起,由于前面的任务还是挂起状态,所以此时新添加的任务(包括同步任务)也处于挂起状态,当主队列执行到加入同步任务的那行代码时,同步任务加入到concurrentQueue队列中,而主队列此时被阻塞(同步任务的特点),且concurrentQueue队列中的任务都处于挂起状态,没有任务可以执行,就陷入了一个类似死锁的状态,点击挂起和唤醒都无效(因为主队列被阻塞了),可以观察结果输出的123和456来判断,可替换区域代码如下。
//通过同步挂起任务
print(123)
concurrentQueue.sync {
self.safeSuspend(self.concurrentQueue)
}
print(456)
输出结果:
start test <_NSMainThread: 0x6000030f8000>{number = 1, name = main} 123 concurrentQueue async task1 <NSThread: 0x60000309f000>{number = 7, name = (null)} concurrentQueue async task2 <NSThread: 0x6000030b5240>{number = 5, name = (null)} 任务挂起了 456 end test <_NSMainThread: 0x6000030f8000>{number = 1, name = main} start test <_NSMainThread: 0x6000030f8000>{number = 1, name = main} 123
任务组相当于一系列任务的松散集合,它可以来自相同或不同队列,扮演着组织者的角色。它可以通知外部队列,组内的任务是否都已完成。或者阻塞当前的线程,直到组内的任务都完成。所有适合组队执行的任务都可以使用任务组,且任务组更适合集合异步任务(如果都是同步任务,直接使用串行队列即可)。
把一个task加入一个DispatchGroup有两种方式
方式一:通过enter()和leave()
let group = DispatchGroup() let queue1 = DispatchQueue(label: "com.1") let queue2 = DispatchQueue(label: "com.2") let queue3 = DispatchQueue(label: "com.3") group.enter() queue1.async(){ for i in 0...10{ print("i = \(i)",Thread.current) } group.leave() } group.enter() queue2.async(){ for j in 11...20{ print("j = \(j)",Thread.current) } group.leave() } group.enter() queue3.async(){ for n in 21...30{ print("n = \(n)",Thread.current) } group.leave() } group.notify(queue: .main){ print("ok") }
方式二:直接把task加入group
let group = DispatchGroup() let queue1 = DispatchQueue(label: "com.1") let queue2 = DispatchQueue(label: "com.2") let queue3 = DispatchQueue(label: "com.3") queue1.async(group: group){ for i in 0...10{ print("i = \(i)",Thread.current) } } queue2.async(group: group){ for j in 11...20{ print("j = \(j)",Thread.current) } } queue3.async(group: group){ for n in 21...30{ print("n = \(n)",Thread.current) } } group.notify(queue: .main){ print("ok") }
如果想让上面异步任务按顺序执行,可以加入信号量机制
let group = DispatchGroup() let queue1 = DispatchQueue(label: "com.1") let queue2 = DispatchQueue(label: "com.2") let queue3 = DispatchQueue(label: "com.3") let semaphore = DispatchSemaphore(value: 1) semaphore.wait() group.enter() queue1.async(group: group){ for i in 0...10{ print("i = \(i)",Thread.current) } group.leave() semaphore.signal() } semaphore.wait() group.enter() queue2.async(group: group){ for j in 11...20{ print("j = \(j)",Thread.current) } group.leave() semaphore.signal() } semaphore.wait() group.enter() queue3.async(group: group){ for n in 21...30{ print("n = \(n)",Thread.current) } group.leave() semaphore.signal() } group.notify(queue: .main){ print("ok") }
两种加入方式在对任务处理的特性上是没有区别的,只是便利之处不同。如果任务所在的队列是自己创建或引用的系统队列,那么直接使用第一种方式直接加入即可。如果任务是由系统或第三方的 API 创建的,由于无法获取到对应的队列,只能使用第二种方式将任务加入组内,例如将 URLSession
的 addDataTask
方法加入任务组中:
extension URLSession {
func addDataTask(to group: DispatchGroup,
with request: URLRequest,
completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void)
-> URLSessionDataTask {
group.enter() // 进入任务组
return dataTask(with: request) { (data, response, error) in
completionHandler(data, response, error)
group.leave() // 离开任务组
}
}
}
等待任务组中的任务全部完成后,可以统一对外发送通知,有两种方式:
1.group.notify
方法,它可以在所有任务完成后通知指定队列并执行一个指定任务,这个通知的操作是异步的(意味着通知后续的代码不需要等待任务,可以继续执行):
let group = DispatchGroup() let queueBook = DispatchQueue(label: "book") queueBook.async(group: group) { // do something 1 } let queueVideo = DispatchQueue(label: "video") queueVideo.async(group: group) { // do something 2 } group.notify(queue: DispatchQueue.main) { print("all task done") } print("do something else.") // 执行结果 // do something else. // do something 1(任务 1、2 完成顺序不固定) // do something 2 // all task done
2.group.wait
方法,它会在所有任务完成后再执行当前线程中后续的代码,因此这个操作是起到阻塞的作用:
let group = DispatchGroup() let queueBook = DispatchQueue(label: "book") queueBook.async(group: group) { // do something 1 } let queueVideo = DispatchQueue(label: "video") queueVideo.async(group: group) { // do something 2 } group.wait() print("do something else.") // 执行结果 // do something 1(任务 1、2 完成顺序不固定) // do something 2 // do something else.
wait方法中还可以指定具体的时间,它表示将等待不超过这个时间,如果任务组在指定时间之内完成则立即恢复当前线程,否则将等到时间结束时再恢复当前线程。
方式1,使用 DispatchTime
,它表示一个时间间隔,精确到纳秒(1/1000,000,000 秒):
let waitTime = DispatchTime.now() + 2.0 // 表示从当前时间开始后 2 秒,数字字面量也可以改为使用 TimeInterval 类型变量
group.wait(timeout: waitTime)
方式2,使用 DispatchWallTime
,它表示当前的绝对时间戳,精确到微秒(1/1000,000 秒),通常使用字面量即可设置延时时间,也可以使用 timespec
结构体来设置一个精确的时间戳。
// 使用字面量设置
var wallTime = DispatchWallTime.now() + 2.0 // 表示从当前时间开始后 2 秒,数字字面量也可以改为使用 TimeInterval 类型变量
DispatchSemaphore
,通常称作信号量,顾名思义,它可以通过计数来标识一个信号,这个信号怎么用呢,取决于任务的性质。通常用于对同一个资源访问的任务数进行限制。
例如,控制同一时间写文件的任务数量、控制端口访问数量、控制下载任务数量等。
信号量的使用非常的简单:
wait
方法让信号量减 1,再安排任务。如果此时信号量仍大于或等于 0,则任务可执行,如果信号量小于 0,则任务需要等待其他地方释放信号。signal
方法增加一个信号量。示例:限制同时运行的任务数
/// 信号量测试类 class DispatchSemaphoreTest { /// 限制同时运行的任务数 static func limitTaskNumber() { let queue = DispatchQueue( label: "com.sinkingsoul.DispatchQueueTest.concurrentQueue", attributes: .concurrent) let semaphore = DispatchSemaphore(value: 2) // 设置数量为 2 的信号量 semaphore.wait() queue.async { task(index: 1) semaphore.signal() } semaphore.wait() queue.async { task(index: 2) semaphore.signal() } semaphore.wait() queue.async { task(index: 3) semaphore.signal() } } /// 任务 static func task(index: Int) { print("Begin task \(index) --->") Thread.sleep(forTimeInterval: 2) print("Sleep for 2 seconds in task \(index).") print("--->End task \(index).") } }
输出结果:示例中设置了同时只能运行 2 个任务,可以看到任务 3 在前两个任务完成后才开始运行(仅当任务执行时间差不多的情况)
Begin task 2 --->
Begin task 1 --->
Sleep for 2 seconds in task 2.
Sleep for 2 seconds in task 1.
--->End task 2.
--->End task 1.
Begin task 3 --->
Sleep for 2 seconds in task 3.
--->End task 3.
在队列和任务组中,任务实际上是被封装为一个 DispatchWorkItem
对象的。任务封装最直接的好处就是可以取消任务。
前面提到的栅栏任务就是通过封装任务对象实现的。
先看看它的创建,其中 qos
、flags
参数都有默认值,可以不填:
let workItem = DispatchWorkItem(qos: .default, flags: DispatchWorkItemFlags()) {
// Do something
}
qos
前面提到过了,这里说一下 DispatchWorkItemFlags
,它有以下几个静态属性(详细解释可参考 官方源码 ):
执行任务时,调用任务项对象的 perform()
方法,这个调用是同步执行的:
workItem.perform()
或则在队列中执行:
let queue = DispatchQueue.global()
queue.async(execute: workItem)
在任务未实际执行之前可以取消任务,调用 cancel()
方法,这个调用是异步执行的:
workItem.cancel()
取消任务将会带来以下结果:
malloc(3)
进行内存分配,而在任务中调用 free(3)
释放。 如果由于取消而从未执行任务,则会导致内存泄露。任务对象也有一个通知方法,在任务执行完成后可以向指定队列发送一个异步调用闭包:
workItem.notify(queue: queue) {
// Do something
}
这个通知方法有一些地方需要注意:
cancel()
方法被取消了,通知也可以生效。任务对象支持等待方法,类似于任务组的等待,也是阻塞型的,需要等待已有的任务完成才能继续执行,也可以指定等待时间:
workItem.perform()
workItem.wait()
workItem.wait(timeout: DispatchTime) // 指定等待时间
workItem.wait(wallTimeout: DispatchWallTime) // 指定等待时间
// 等待任务完成
// do something
下面看个完整的例子:
示例 12.1:任务对象测试。
/// 任务对象测试 @IBAction func dispatchWorkItemTestButtonTapped(_ sender: Any) { DispatchWorkItemTest.workItemTest() } /// 任务对象测试类 class DispatchWorkItemTest { static func workItemTest() { var value = 10 let workItem = DispatchWorkItem { print("workItem running start.--->") value += 5 print("value = ", value) print("--->workItem running end.") } let queue = DispatchQueue.global() queue.async(execute: workItem) queue.async { print("异步执行 workItem") workItem.perform() print("任务2取消了吗:\(workItem.isCancelled)") workItem.cancel() print("异步执行 workItem end") } workItem.notify(queue: queue) { print("notify 1: value = ", value) } workItem.notify(queue: queue) { print("notify 2: value = ", value) } workItem.notify(queue: queue) { print("notify 3: value = ", value) } queue.async { print("异步执行2 workItem") Thread.sleep(forTimeInterval: 2) print("任务3取消了吗:\(workItem.isCancelled)") workItem.perform() print("异步执行2 workItem end") } } }
执行结果,可以看到任务第一次执行完成后,发出了 3 次通知,而且未按照代码的顺序。在发出通知前,任务还有一次执行未完成,并未造成通知报错。第二次执行任务后,取消了任务,因此任务第三次未正常执行:
workItem running start.--->
异步执行 workItem
异步执行2 workItem
value = 15
workItem running start.--->
value = 20
--->workItem running end.
任务2取消了吗:false
异步执行 workItem end
notify 2: value = 20
notify 3: value = 20
notify 1: value = 20
--->workItem running end.
任务3取消了吗:true
异步执行2 workItem end
另外关于DispatchSource、DispatchIO、DispatchData、时间相关结构体说明可以参考这篇文章,本文结构参考了很多大佬的文章,属实是站在大佬肩膀上了,但其中有很多自己的思考和对其他文章的改正,值得一看。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。