网站建设人员培训,广州专业的网站建设公司,个人网站不备案做经营性质网站,鹤壁集团网站建设GCD 文章目录 GCD进程线程进程与线程的关系进程与线程的区别 任务#xff08;执行的代码#xff09;队列线程与队列的关系 队列任务**同步执行任务#xff08;sync#xff09;**辅助方法**异步执行任务#xff08;async)**总结栅栏任务迭代任务 队列详细属性QoSAttributes…GCD 文章目录 GCD进程线程进程与线程的关系进程与线程的区别 任务执行的代码队列线程与队列的关系 队列任务**同步执行任务sync**辅助方法**异步执行任务async)**总结栅栏任务迭代任务 队列详细属性QoSAttributesAutoreleaseFrequencyTarget 延迟加入队列挂起和唤醒队列GCD Group任务组加入任务组任务组通知 信号量任务对象创建任务执行任务取消任务任务通知任务等待代码示例 其他 GCD 全称是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.main4.系统全局并发队列
全局并发队列存在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}
}队列任务
同步执行任务sync
必须等待当前任务执行完毕才会执行下一条任务任务一经提交就会阻塞当前队列若是并发队列理解为阻塞当前行并请求队列立即安排执行该同步任务即线程切换到另一个队列执行执行完毕回到当前队列不具备开启多线程的能力
假设当前在主队列中执行此时存在一个手动创建的串行队列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 serialcase concurrentcase maincase global}//定义队列let serialQueue DispatchQueue(label: com.serialQueue)let concurrentQueue DispatchQueue(label: com.concurrentQueue, attributes: .concurrent)let mainQueue DispatchQueue.mainlet globalQueue DispatchQueue.global()//定义队列keylet serialQueueKey DispatchSpecificKeyString()let concurrentQueueKey DispatchSpecificKeyString()let mainQueueKey DispatchSpecificKeyString()let globalQueueKey DispatchSpecificKeyString()//初始化队列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: DispatchSpecificKeyString){let value DispatchQueue.getSpecific(key: key)let opnValue: String? queueType.rawValueprint(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异步执行任务async)
不用等待当前任务执行完毕就可以执行下一条任务具备开启多线程的能力特性任务提交后不会阻塞当前队列会由队列安排另一个线程执行
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 concurrentQueuelet 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 inif 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)
QoS
队列在执行上是有优先级的更高的优先级可以享受更多的计算资源从高到低包含以下几个等级 userInteractiveuserInitiateddefaultutilitybackground Attributes
包含两个属性
concurrent标识队列为并行队列initiallyInactive标识运行队列中的任务需要动手触发未添加此标识时向队列中添加任务会自动运行触发时通过 queue.activate() 方法。
AutoreleaseFrequency
这个属性表示 autorelease pool 的自动释放频率 autorelease pool 管理着任务对象的内存周期。
包含三个属性
inherit继承目标队列的该属性workItem跟随每个任务的执行周期进行自动创建和释放never不会自动创建 autorelease pool需要手动管理。
一般任务采用 .workItem 属性就够了特殊任务如在任务内部大量重复创建对象的操作可选择 .never 属性手动创建 autorelease pool。
Target
这个属性设置的是一个队列的目标队列即实际将该队列的任务放入指定队列中运行。目标队列最终约束了队列优先级等属性。
在程序中手动创建的队列其实最后都指向系统自带的 主队列 或 全局并发队列。
手动创建队列的好处是可以将任务进行分组管理。如单独阻塞队列中的任务而不是阻塞系统队列中的全部任务。如果阻塞了目标队列所有指向它的原队列也将被阻塞。
在 Swift 3 及之后对目标队列的设置进行了约束只有两种情况可以显式地设置目标队列原因参考
初始化方法中指定目标队列。初始化方法中attributes 设定为 initiallyInactive然后在队列执行 activate() 之前可以指定目标队列。
在其他地方都不能再改变目标队列。
延迟加入队列
等待一段时间后再进入队列中这时候可以使用 asyncAfter 方法.
class AsyncAfter {/// 延迟执行闭包static func dispatch_later(_ time: TimeInterval, block: escaping ()-()) {let t DispatchTime.now() timeDispatchQueue.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 Foundationclass 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 1queue.suspend()print(任务挂起了)}/// 安全的唤醒操作func safeResume(_ queue: DispatchQueue) {if suspendCount 1 {queue.resume()suspendCount 0print(任务唤醒了)} else if suspendCount 1 {print(唤醒的次数过多)} else {queue.resume()suspendCount - 1print(唤醒的次数不够还需要 \(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 task1NSThread: 0x6000007d9100{number 6, name (null)}
concurrentQueue async task2NSThread: 0x6000007d5100{number 3, name (null)}start test_NSMainThread: 0x60000079c540{number 1, name main}
任务挂起了
end test _NSMainThread: 0x60000079c540{number 1, name main}唤醒的次数不够还需要 1 次唤醒。任务唤醒了
concurrentQueue async task3NSThread: 0x600000797300{number 4, name (null)}
concurrentQueue async task5NSThread: 0x6000007f7e40{number 9, name (null)}
concurrentQueue async task4NSThread: 0x6000007d1640{number 8, name (null)}
concurrentQueue async task1NSThread: 0x600000797300{number 4, name (null)}
concurrentQueue async task2NSThread: 0x6000007d1380{number 10, name (null)}
concurrentQueue async task3NSThread: 0x6000007f7e40{number 9, name (null)}
concurrentQueue async task4NSThread: 0x6000007d11c0{number 11, name (null)}
concurrentQueue async task5NSThread: 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 task1NSThread: 0x600003c38280{number 3, name (null)}
end test _NSMainThread: 0x600003c68480{number 1, name main}
concurrentQueue async task2NSThread: 0x600003c11440{number 8, name (null)}
任务挂起了start test_NSMainThread: 0x600003c68480{number 1, name main}
end test _NSMainThread: 0x600003c68480{number 1, name main}任务唤醒了
concurrentQueue async task3NSThread: 0x600003c11440{number 8, name (null)}
concurrentQueue async task4NSThread: 0x600003c0fe80{number 9, name (null)}
concurrentQueue async task5NSThread: 0x600003c38280{number 3, name (null)}
concurrentQueue async task1NSThread: 0x600003c0f600{number 10, name (null)}
concurrentQueue async task2NSThread: 0x600003c11440{number 8, name (null)}
任务挂起了任务唤醒了
concurrentQueue async task3NSThread: 0x600003c11440{number 8, name (null)}
concurrentQueue async task4NSThread: 0x600003c0f600{number 10, name (null)}
concurrentQueue async task5NSThread: 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 task1NSThread: 0x60000309f000{number 7, name (null)}
concurrentQueue async task2NSThread: 0x6000030b5240{number 5, name (null)}
任务挂起了
456
end test _NSMainThread: 0x6000030f8000{number 1, name main}start test_NSMainThread: 0x6000030f8000{number 1, name main}
123
GCD Group任务组
任务组相当于一系列任务的松散集合它可以来自相同或不同队列扮演着组织者的角色。它可以通知外部队列组内的任务是否都已完成。或者阻塞当前的线程直到组内的任务都完成。所有适合组队执行的任务都可以使用任务组且任务组更适合集合异步任务如果都是同步任务直接使用串行队列即可。
加入任务组
把一个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) incompletionHandler(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 done2.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它有以下几个静态属性详细解释可参考 官方源码
assignCurrentContext: 标记应该为任务分配创建它时的上下文属性例如QoS、os_activity_t、可能存在的当前 IPC 请求属性。如果直接调用任务任务对象将在它的持续时间内在调用线程中应用这些属性。如果提交任务至队列中则会替换提交任务时的上下文属性默认值。barrier: 标记任务为栅栏任务提交至并行队列时生效如果直接运行该任务对象则无此效果。detached: 标记任务在执行时应该剥离当前执行上下文属性例如QoS、os_activity_t、可能存在的当前 IPC 请求属性。如果直接调用任务任务对象将在它的持续时间内从调用线程中删除这些属性如果存在属性且应用于任务之前。如果提交任务至队列中将使用队列的属性或专门分配给任务对象的任何属性进行执行。如果创建任务时指定了 QoS则该 QoS 将优先于 flag 对应的 QoS 值。enforceQoS: 标记任务提交至队列执行时任务对象被分配的 QoS 提交任务时的值应优先于队列的 QoS这样做不会降低 QoS。当任务提交至队列同步执行时或则直接执行任务时这个 flag 是默认值。inheritQoS: 标记任务提交至队列执行时队列的 QoS 应优先于任务对象被分配的 QoS 提交任务时的值后一个 QoS 值只会在队列的 QoS 有问题时才会采用这样做会导致 QoS 不会低于继承自队列的 QoS。当任务提交至队列异步执行时这个 flag 是默认值且直接执行任务时该标志无效。noQoS: 标记任务不应指定 QoS如果直接执行将以调用线程的 QoS 执行。如果提交至队列则会替换提交任务时的 QoS 默认值。
执行任务
执行任务时调用任务项对象的 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 10let workItem DispatchWorkItem {print(workItem running start.---)value 5print(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、时间相关结构体说明可以参考这篇文章本文结构参考了很多大佬的文章属实是站在大佬肩膀上了但其中有很多自己的思考和对其他文章的改正值得一看。