GCD:异步同步?串行并发?一文轻松拿捏!

GCD


GCD:

全称是Grand Central Dispatch,底层是纯c语言,GCD 的核心就是为了解决如何让程序有序、高效的运行

GCD的优势:

GCD是苹果公司为多核的并行运算提出的解决方案

GCD会自动利用更多的CPU内核(比如双核、四核)

GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程)

程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码

前导知识:

进程

进程是系统中正在运行的一个程序,程序一旦运行就是进程

线程

线程进程中执行运算的最小单位,负责当前进程中程序的执行。

进程与线程的关系

一个进程至少有一个线程,一个进程可以运行多个线程,同一进程的多个线程可共享数据。

进程与线程的区别

进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。

任务(执行的代码)

**任务:**一段代码、一个API调用、一个方法、函数、闭包等,一个应用就是由很多任务组成。

任务执行时间: 任务执行时间与线程状态、CPU调度、线程池调度、队列的优先级、任务的复杂度有关。

队列

**队列(排队处理的任务):**FIFO(先进先出)先排队的先受理。

线程与队列的关系

队列是用于保存以及管理任务的,线程负责去队列中取任务进行执行。也可以理解为队列调度任务给到线程中执行

1.串行队列:

​ 串行队列(DISPATCH_QUEUE_SERIAL) : 每次只有一个任务被执行。让任务一个接着一个地执行。一般只开启一个线程,一个任务执行完毕后,再执行下一个任务(特例在后面)。

重要特征:串行队列中执行任务不允许被当前队列中的任务阻塞(此时会死锁),但可以被别的队列任务阻塞。

304f3c68b6654676876391fd6aa199d9tplv-k3u1fbpfcp-zoom-in-crop-mark1512000

创建串行队列:

let serialQueue = DispatchQueue(label: "com.xxx.xxx.queueName")

2.并发队列:

​ 并发队列(DISPATCH_QUEUE_CONCURRENT) : 放到并发队列的任务,GCD也会 FIFO的取出来,放在多个线程中执行,看起来,所有的任务都是一起执行的。

**重要特征:**系统会为并行队列至少分配一个线程,队列允许被任何队列的任务阻塞。

aad73e9709f04f1eb06fe8d4872baea1tplv-k3u1fbpfcp-zoom-in-crop-mark1512000

创建并发队列:

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
    }
}

队列任务

同步执行任务(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)
        }

报错:

4febf9da44059bafecf904dce08e6089

serialQueue死锁:

let serialQueue = DispatchQueue(label: ".com1")
        serialQueue.sync {
            print("同步1",Thread.current)
            serialQueue.sync {
                print("同步2",Thread.current)
            }
        }

报错:

ade6d3e3d8d11ca24e6a7d450b23caa0

​ 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

异步执行任务(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 : 不会阻塞当前线程 任务取出来放到其他线程中 所以会创建很多线程 由系统控制

默认代码为串行同步,网络请求为并发异步,这两个组合为常用组合

栅栏任务

162a8a44af2c49d2tplv-t2oaga2asx-jj-mark3024000q75

​ 栅栏任务的主要特性是可以对队列中的任务进行阻隔,执行栅栏任务时,它会先等待队列中已有的任务全部执行完成,然后它再执行,在它之后加入的任务也必须等栅栏任务执行完后才能执行。

​ 这个特性更适合并行队列,而且对栅栏任务使用同步或异步方法效果都相同。

创建方式,先创建 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)

QoS

队列在执行上是有优先级的,更高的优先级可以享受更多的计算资源,从高到低包含以下几个等级:


  • userInteractive
  • userInitiated
  • default
  • utility
  • background

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() + 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

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 创建的,由于无法获取到对应的队列,只能使用第二种方式将任务加入组内,例如将 URLSessionaddDataTask 方法加入任务组中:

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 对象的。任务封装最直接的好处就是可以取消任务。

前面提到的栅栏任务就是通过封装任务对象实现的。

创建任务

先看看它的创建,其中 qosflags 参数都有默认值,可以不填:

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 = 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、时间相关结构体说明可以参考这篇文章,本文结构参考了很多大佬的文章,属实是站在大佬肩膀上了,但其中有很多自己的思考和对其他文章的改正,值得一看。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值