Swift 5.1 - GCD使用总结

在swift中GCD采用链式调用,较OC而言使用方式更为简单,可读性更高。全文代码均默认在主线程中执行。

队列的获取与创建

        //串行队列
        let serial = DispatchQueue(label: "serial",attributes: .init(rawValue:0))
        //并发队列
        let concurrent = DispatchQueue(label: "serial",attributes: .concurrent)
        //主队列
        let mainQueue = DispatchQueue.main
        //全局队列
        let global = DispatchQueue.global()

GCD队列都遵循先进先出(FIFO)。所以往并发队列中添加同步任务,其执行顺序和任务的添加顺序相同。全局队列在功能上和并发队列是等价的,所以需要并发队列时,首选使用系统的全局队列。

这里要注意一点并发队列不能称为并行队列。请参考并发和并行的区别

同步任务与异步任务

主队列+同步任务——死锁

同步任务会阻塞线程,在如下代码中需要优先执行print(2),等待其执行后才能继续往下,但是主队列为串行队列,需要等待当前任务执行完成后才能执行后加入队列的print(2)任务,造成相互等待。

        print(1)
        DispatchQueue.main.sync {
            print(2)
        }
        print(3)
主队列+异步任务——依次执行(不开启新线程)

以下代码中的print(2)任务会添加到主队列的最后。又由于主线程+异步任务不会开启新线程,以下代码输出1 3 2,顺序固定不变。

        print(1)
        DispatchQueue.main.async {
            print(2)
        }
        print(3)
串行队列+同步任务——依次执行

以下代码不管每个任务休眠时间多长,输出顺序始终为0...10

        //串行队列
        let serial = DispatchQueue(label: "serial",attributes: .init(rawValue:0))
        for i in 0...10 {
            serial.sync {
                sleep(arc4random()%3)//休眠时间随机
                print(i)
            }
        }
串行队列+异步任务——开启一个新线程依次执行

以下代码输出顺序始终为0...10,并且for循环中的Thread.current的输出始终为同一个新线程

        //串行队列
        let serial = DispatchQueue(label: "serial",attributes: .init(rawValue:0))
        print(Thread.current)//主线程
        for i in 0...10 {
            serial.async {
                sleep(arc4random()%3)//休眠时间随机
                print(i,Thread.current)//子线程
            }
        }
并发队列+同步任务——依次执行

以下代码输出顺序始终为0...10,且线程始终为主线程

        for i in 0...10 {
            DispatchQueue.global().sync {
                sleep(arc4random()%3)//休眠时间随机
                print(i,Thread.current)
            }
        }
并发队列+异步任务——开启多个线程并发执行

以下代码输出顺序随机,且线程信息不同。注意:这里可能不会输出11个不同的线程信息,经过代码测试发现当一个线程的任务执行完成后,如果队列中还有任务,此线程会继续被调度执行后续任务。 将任务数增多,结果更明显。

        for i in 0...10 {
            DispatchQueue.global().async {
                sleep(arc4random()%3)//休眠时间随机
                print(i,Thread.current)
            }
        }
并发队列——最大并发数

GCD并不能无限制的创建线程,如下代码其实最多创建64个子线程,意味着最大并发数为64。参考

       for i in 0...1000 {
            DispatchQueue.global().async {
                print(i,Thread.current)
                sleep(10000)
            }
        }
GCD 栅栏

在swift中栅栏不再是一个单独的方法。而是DispatchWorkItemFlags结构体中的一个属性。sync/async方法的其中一个参数类型即为DispatchWorkItemFlags,所以使用代码如下。这样的调用方式可以更好的理解栅栏,其实它就是一个分隔任务,将其添加到需要栅栏的队列中,以分隔添加前后的其他任务。以下代码栅栏前后均为并发执行。如果将添加栅栏修改为sync则会阻塞当前线程。

        for i in 0...10 {
            DispatchQueue.global().async {
                print(i)
            }
        }
        DispatchQueue.global().async(flags: .barrier) {
            print("this is barrier")
        }
        for i in 11...20 {
            DispatchQueue.global().async {
                print(i)
            }
        }
GCD group

队列组一般用来处理任务的依赖,比如需要等待多个网络请求返回后才能继续执行后续任务。

使用notify添加结束任务

必须要等待group中的任务执行完成后才能执行,无法定义超时。

    override func viewDidLoad() {
        let group = DispatchGroup()
        for i in 0...10 {
            DispatchQueue.global().async(group: group) {
                sleep(arc4random()%3)//休眠时间随机
                print(i)
            }
        }
        //queue参数表示以下任务添加到的队列
        group.notify(queue: DispatchQueue.main) {
            print("group 任务执行结束")
        }
    }
使用wait进行等待——可定义超时
        let group = DispatchGroup()
        for i in 0...10 {
            DispatchQueue.global().async(group: group) {
                sleep(arc4random()%10)//休眠时间随机
                print(i)
            }
        }
        switch group.wait(timeout: DispatchTime.now()+5) {
        case .success:
            print("group 任务执行结束")
        case .timedOut:
            print("group 任务执行超时")
        }
enter()leave()

enter()是标示一个任务加入到队列,leave()标识一个队列中的任务执行完成。
注意:enter()leave()必须成对调用
如果enter()后未调用leave()则不会触发notify,wait也只会timeout。
如果leave()之前没有调用enter()则会引起crash。

信号量 semaphore

GCD 中的信号量是指 Dispatch Semaphore,是持有计数的信号。类似于过高速路收费站的栏杆。可以通过时,打开栏杆,不可以通过时,关闭栏杆。在 Dispatch Semaphore 中,使用计数来完成这个功能,计数为0时等待,不可通过。计数为1或大于1时,计数减1且不等待,可通过。

        let semaphore = DispatchSemaphore(value: 0)//创建一个信号量,并初始化信号总量
        semaphore.signal()//发送一个信号让信号量加1
        semaphore.wait()//可以使总信号量减1,当信号总量为0时就会一直等待(阻塞所在线程),否则就可以正常执行。
信号量处理线程同步

将异步执行任务转化为同步执行任务。如必须等待异步的网络请求返回后才能执行后续任务时。

        let semaphore = DispatchSemaphore(value: 0)
        DispatchQueue.global().async {
            sleep(arc4random()%5)//休眠时间随机
            print("completed")
            semaphore.signal()
        }
        switch semaphore.wait(timeout: DispatchTime.now()+10) {//信号量为0,调用wait后阻塞线程
        case .success:
            print("success")
        case .timedOut:
            print("timeout")
        }
        print("over")
信号量控制最大并发数

在Operation中可以通过maxConcurrentOperationCount轻松实现控制最大并发数,GCD中需要借助信号量实现。以下代码就限制了最多两个任务并发执行。

        let semaphore = DispatchSemaphore(value: 2)
        for i in 0...10 {
            semaphore.wait()//当信号量为0时,阻塞在此
            DispatchQueue.global().async {
                sleep(3)
                print(i,Thread.current)
                semaphore.signal()//信号量加1
            }
            print("=======================")
        }
使用DispatchSemaphore加锁

非线程安全,即当一个变量可能同时被多个线程修改。以下代码如果不使用信号量输出是随机值。

        let semaphore = DispatchSemaphore(value: 1)
        var i = 0
        for _ in 1...10 {
            DispatchQueue.global().async {
                semaphore.wait()//当信号量为0时,阻塞在此
                for _ in 1...10 {
                    i += 1
                }
                print(i)
                semaphore.signal()//信号量加1
            }
        }
延时任务

使用GCD执行延时任务指定的并不是任务的执行时间,而是加入队列的时间。所以执行时间可能不太精确。但是任务是通过闭包加入,相较performSelectorAfterDelay可读性更好,也更安全。

        DispatchQueue.global().asyncAfter(deadline: DispatchTime.now()+2) {
            print("延时任务")
        }
dispatch_once

在swift3以后dispatch_once被废弃。swift中有更好的定义单例和一次性代码的方式。

DispatchWorkItem与任务取消

DispatchWorkItem其实就是用来代替OC中的dispatch_block_t。如果任务是通过DispatchWorkItem定义。在执行之前,可以执行取消操作。注意即使任务已经加入队列,只要还未执行就可以进行取消,但是无法判断任务在队列中的状态,所以一般会根据加入队列的时间确定是否可以取消。

        let workItem = DispatchWorkItem {
            print("延时任务")
        }
        DispatchQueue.global().asyncAfter(deadline: DispatchTime.now()+2, execute: workItem)
        sleep(1)
        workItem.cancel()
DispatchWorkItem主动执行
        let workItem = DispatchWorkItem {
            print("workItem")
        }
        workItem.perform()
等待DispatchWorkItem执行完成
        let workItem = DispatchWorkItem {
            sleep(3)
            print("workItem")
        }
        DispatchQueue.global().async(execute: workItem)
        switch workItem.wait(timeout: DispatchTime.now()+5) {
        case .success:
            print("success")
        case .timedOut:
            print("timeout")
        }
DispatchWorkItem执行完成通知
        let workItem = DispatchWorkItem {
            sleep(3)
            print("workItem")
        }
        DispatchQueue.global().async(execute: workItem)
        workItem.notify(queue: DispatchQueue.main) {
            print("completed")
        }

参考:

GCD最大线程数

iOS 多线程:『GCD』详尽总结

推荐阅读更多精彩内容