Swift5 多线程 - GCD

在软件开发中,我们经常会遇到大量计算,或者数据很多的下载,这些都需要异步线程处理。还有一些有先后顺序的操作,这些要到队列的处理。另外,App 的 UI 更新也必须要回到主线程中处理。Swift 实现多线程的方法主要有三种 NSThreadNSOperationQueueGCD,不过我们通常用得比较多的是 GCD 方式

GCD(Grand Central Dispatch)

Grand Central Dispatch (GCD) 是 Apple 开发的一个多核编程的解决方法,基本概念就是dispatch queue(调度队列),queue 是一个对象,它可以接受任务,并将任务以先到先执行的顺序来执行。dispatch queue 可以是并发的或串行的。GCD 的底层依然是用线程实现,不过我们可以不用关注实现的细节。Swift3 之前 GCD 仍是面向过程的写法,所以需要封装一层再使用。Swift3 苹果打成Dispatch 这个 module.你可以通过 import 进行导入再使用。Swift4 之后,我们可以直接使用。其优点有如下几点:

  1. 易用:GCD 比 thread更简单易用。基于 block 的特效使它能极为简单地在不同代码作用域之间传递上下文。
  2. 效率:GCD 实现功能轻量,优雅,使得它在很多地方比专门创建消耗资源的线程更加实用且快捷。
  3. 性能:GCD 自动根据系统负载来增减线程数量,从而减少了上下文切换并增加了计算效率。
  4. 安全:无需加锁或其他同步机制。

GCD 可用于多核的并行运算
GCD 会自动利用更多的CPU内核(比如双核、四核)
GCD 会自动管理线程的生命周期(创建线程、调度任务、销毁线程)

队列和任务

* GCD 中有2个核心概念
    任务:执行什么操作
    队列:用来存放任务

* GCD 的使用就2个步骤
    定制任务
    确定想做的事情

* 将任务添加到队列中
    GCD 会自动将队列中的任务取出,放到对应的线程中执行
    任务的取出遵循队列的 FIFO 原则:先进先出,后进后出

DispatchQueue

Dispatch 会自动的根据 CPU 的使用情况,创建线程来执行任务,并且自动的运行到多核上,提高程序的运行效率。对于开发者来说,在 GCD 层面是没有线程的概念的,只有队列(queue)。任务都是以block 的方式提交到对列上,然后 GCD 会自动的创建线程池去执行这些任务。

DispatchQueue 是一个类似线程的概念,这里称作对列队列是一个 FIFO(先进先出,后进后出) 数据结构,意味着先提交到队列的任务会先开始执行。DispatchQueue 背后是一个由系统管理的线程池。

* 同步和异步的区别
    同步(`sync`):只能在当前线程中执行任务,不具备开启新线程的能力
    异步 (`async`):可以在新的线程中执行任务,具备开启新线程的能力

各种队列的执行效果

并发队列 手动创建的串行队列 主队列
同步(sync) 没有开启新线程、串行执行任务 没有开启新线程、串行执行任务 没有开启新线程、串行执行任务
异步(async) 有开启新线程、并发执行任务 有开启新线程、串行执行任务 没有开启新线、串行执行任务
基本写法,异步执行回主线程写法
    DispatchQueue.global().async {
        print("异步做某事: \(Thread.current)")
        DispatchQueue.main.async {
            print("回到主线程: \(Thread.current)")
        }
    }

DispatchQueue

一个对象,用于在应用程序的主线程或后台线程上串行或并发地管理任务的执行。

    let queue = DispatchQueue(label: "labelname", qos: .default, attributes: .concurrent, autoreleaseFrequency: .inherit)
  • label 队列的标识符,方便调试
  • qos 队列的 quality of service。用来指明队列的 “重要性”,后文会详细讲到。
  • attributes 队列的属性。类型是DispatchQueue.Attributes,是一个结构体,遵循了协议OptionSet。意味着你可以这样传入第一个参数[.option1,.option2]
  • autoreleaseFrequency。顾名思义,自动释放频率。有些队列是会在执行完任务后自动释放的,有些比如 Timer 等是不会自动释放的,是需要手动释放。

队列分类

* 系统创建的队列
    * 主队列(对应主线程)
    * 全局队列
* 用户创建的队列
* 串行与并行
    // 主队列 
    let mainQueue = DispatchQueue.main
    // 全局队列
    let globalQueue = DispatchQueue.global()
    // 用户创建的队列
    let globalQueueWithQos = DispatchQueue.global(qos: .userInitiated)

串行与并行

串行(serial) :一次只能干一件事,挨个按顺序执行


Swift5 多线程 - GCD_第1张图片
串行.jpg

并行(concurrent):多条流水线同时工作


Swift5 多线程 - GCD_第2张图片
并行.jpg
    /*
        默认:列队是串行的
        .concurrent:列队是并发的
        .initiallyInactive:列队不会自动执行,需要开发中手动触发
    */
    // 串行队列 
    let serialQueue = DispatchQueue(label: "serialQueue")
    // 并行队列
    let concurrentQueue = DispatchQueue(label: "concurrentQueue",attributes:.concurrent)

异步与同步

  • 异步(async)提交一段任务到队列,并且立刻返回
    func serial() {
        // 异步
        let serialQueue = DispatchQueue(label: "serialQueue")
                print("\(Thread.current) Main queue Start")
                serialQueue.async {
                    self.readDataTask(label: "1")
                }
                serialQueue.async {
                    self.readDataTask(label: "2")
                }
                print("\(Thread.current) Main queue End")
    }
    
    // 读数据
    func readDataTask(label: String){
        print("\(Thread.current) Start sync task\(label)")
        sleep(2)
        print("\(Thread.current) End sync task\(label)")
    }
    
/* 输出结果
{number = 1, name = main} Main queue Start
{number = 4, name = (null)} Start sync task1
{number = 1, name = main} Main queue End
{number = 4, name = (null)} End sync task1
{number = 4, name = (null)} Start sync task2
{number = 4, name = (null)} End sync task2
    */

系统在导步串行队列中开了一个线程4处理,可以看到每一条线程,是完成 task1 之后才会开始 task2,而且主线程线程1没有阻塞的运行完。
总结一下,主线程按照顺序提交任务1,任务2到 serialQueue,瞬间执行完毕,并没有被阻塞。
在 serialQueue 上先执行任务1,任务1执行完毕后再执行任务2.

  • 同步 (sync) 提交一段任务到队列,并且阻塞当前线程,任务结束后当前线程继续执行
    func concurrent() {
        print("\(Thread.current) Main queue Start")
        // 同步
        let concurrentQueue = DispatchQueue(label: "concurrent", attributes: .concurrent)
        concurrentQueue.async {
            self.readDataTask(label: "3")
        }
        concurrentQueue.async {
            self.readDataTask(label: "4")
        }
        print("\(Thread.current) Main queue End")
    }
    
    // 读数据
    func readDataTask(label: String){
        print("\(Thread.current) Start sync task\(label)")
        sleep(2)
        print("\(Thread.current) End sync task\(label)")
    }
    
    /* 输出结果
{number = 1, name = main} Main queue Start
{number = 1, name = main} Main queue End
{number = 4, name = (null)} Start sync task4
{number = 5, name = (null)} Start sync task3
{number = 5, name = (null)} End sync task3
{number = 4, name = (null)} End sync task4
    */

主线程依然没有被阻塞。
在 concurrentQueue 队列上,两个任务按照提交的次序开始,两个任务并发的执行了。

sync 是一个强大但是容易被忽视的函数。使用 sync,可以方便的进行线程间同步。但是,有一点要注意,sync 容易造成死锁.

DispatchQoS (quality of service) 服务质量

适用于任务的服务质量或执行优先级。

优先级由最低的 background 到最高的 userInteractive 共五个,还有一个为定义的 unspecified.

  • background:最低优先级,等同于 DISPATCH_QUEUE_PRIORITY_BACKGROUND. 用户不可见,比如:在后台存储大量数据
  • utility:优先级等同于 DISPATCH_QUEUE_PRIORITY_LOW,可以执行很长时间,再通知用户结果。比如:下载一个大文件,网络,计算
  • default:默认优先级,优先级等同于 DISPATCH_QUEUE_PRIORITY_DEFAULT,建议大多数情况下使用默认优先级
  • userInitiated:优先级等同于 DISPATCH_QUEUE_PRIORITY_HIGH,需要立刻的结果
  • userInteractive:用户交互相关,为了好的用户体验,任务需要立马执行。使用该优先级用于 UI 更新,事件处理和小工作量任务,在主线程执行

Qos指定了列队工作的优先级,系统会根据优先级来调度工作,越高的优先级能够越快被执行,但是也会消耗功能,所以准确的指定优先级能够保证app有效的使用资源。

QoS 可以在创建 queue 时添加或者在提交 block 的时候,指定 QoS

    DispatchQueue.global().async(qos: .background) {
          // code
    }

DispatchWorkItem

想要执行的工作以某种方式进行封装,使您可以附加完成句柄或执行依赖项。通俗的说就是 DispatchWorkItem 把任务封装成一个对象。

    let item = DispatchWorkItem {
        // 任务
    }
    DispatchQueue.global().async(execute: item)

也可以初始化时指定更多的参数

    DispatchWorkItem(qos: .userInitiated, flags: [.assignCurrentContext,.barrier]) {
            // 任务
    }
  • 第一个参数表示 QoS。
  • 第二个参数类型为 DispatchWorkItemFlags。指定这个任务的配饰信息
  • 第三个参数则是实际的任务 block

DispatchWorkItemFlags 的参数分为两组

执行情况

  • barrier 屏障,后面详解
  • detached
  • assignCurrentContext

QoS覆盖信息

  • noQoS // 没有QoS
  • inheritQoS // 继承Queue的QoS
  • enforceQoS // 自己的QoS覆盖Queue

DispatchGroup

DispatchGroup 用于管理一组任务的执行,然后监听任务的完成,进而执行后续操作。比如:同一个页面发送多个网络请求,等待所有结果请求成功刷新 UI 界面

notify(依赖任务)

    func groupNotify() {
        let queue = DispatchQueue.global()
        let group = DispatchGroup()
        queue.async(group: group, qos: .default, flags: [], execute: {
            for _ in 0...4 {
                print("\(Thread.current) 耗时任务一")
            }
        })
        queue.async(group: group, qos: .default, flags: [], execute: {
            for _ in 0...4 {
                print("\(Thread.current) 耗时任务二")
            }
        })
        // 执行完上面的两个耗时操作, 回到 queue 队列中执行下一步的任务
        group.notify(queue: queue) {
            print("\(Thread.current) 回到该队列中执行")
        }
    }
    
    /* 输出结果
{number = 3, name = (null)} 耗时任务一
{number = 5, name = (null)} 耗时任务二
{number = 3, name = (null)} 耗时任务一
{number = 3, name = (null)} 耗时任务一
{number = 5, name = (null)} 耗时任务二
{number = 3, name = (null)} 耗时任务一
{number = 3, name = (null)} 耗时任务一
{number = 5, name = (null)} 耗时任务二
{number = 5, name = (null)} 耗时任务二
{number = 5, name = (null)} 耗时任务二
{number = 5, name = (null)} 回到该队列中执行
    */

wait(任务等待)

    func groupWait() {
        let queue = DispatchQueue.global()
        let group = DispatchGroup()
        queue.async(group: group, qos: .default, flags: [], execute: {
            for _ in 0...4 {
                print("\(Thread.current) 耗时任务一")
            }
        })
        queue.async(group: group, qos: .default, flags: [], execute: {
            for _ in 0...4 {
                print("\(Thread.current) 耗时任务二")
                sleep(1)
            }
        })
        // 等待上面任务执行,会阻塞当前线程,超时就执行下面的,上面的继续执行。可以无限等待 .distantFuture
        let result = group.wait(timeout: .now()+10)
        switch result {
        case .success:
            print("\(Thread.current) 不超时, 上面的两个任务都执行完")
        case .timedOut:
            print("\(Thread.current) 超时了, 上面的任务还没执行完执行这了")
        }
        
        print("\(Thread.current) 完成...")
    }
    
    /* 输出结果
{number = 3, name = (null)} 耗时任务一
{number = 5, name = (null)} 耗时任务二
{number = 3, name = (null)} 耗时任务一
{number = 3, name = (null)} 耗时任务一
{number = 3, name = (null)} 耗时任务一
{number = 3, name = (null)} 耗时任务一
{number = 5, name = (null)} 耗时任务二
{number = 5, name = (null)} 耗时任务二
{number = 5, name = (null)} 耗时任务二
{number = 5, name = (null)} 耗时任务二
{number = 1, name = main} 不超时, 上面的两个任务都执行完
{number = 1, name = main} 完成...
    */

手动管理 group 的计数器,enterleave 必须配对

由于是并发执行异步任务,所以任务的先后次序是不一定的,看起来符合我们的需求,最后接受通知然后可以刷新 UI 操作。但是真实的网络请求是异步、耗时的,并不是立马就返回,所以我们使用 asyncAfter 模拟延时看看,将任务1延时一秒执行:

   // 将任务1延时一秒执行
   func groupNotify() {
        let queue = DispatchQueue.global()
        let group = DispatchGroup()
        queue.async(group: group, qos: .default, flags: [], execute: {
            // 增加耗时
            DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
                 for _ in 0...4 {
                     print("\(Thread.current) 耗时任务一")
                 }
            })
        })
        queue.async(group: group, qos: .default, flags: [], execute: {
            for _ in 0...4 {
                print("\(Thread.current) 耗时任务二")
            }
        })
        // 执行完上面的两个耗时操作, 回到 queue 队列中执行下一步的任务
        group.notify(queue: queue) {
            print("\(Thread.current) 回到该队列中执行")
        }
    }
    
    /* 输出结果
{number = 6, name = (null)} 耗时任务二
{number = 6, name = (null)} 耗时任务二
{number = 6, name = (null)} 耗时任务二
{number = 6, name = (null)} 耗时任务二
{number = 6, name = (null)} 耗时任务二
{number = 7, name = (null)} 回到该队列中执行
{number = 1, name = main} 耗时任务一
{number = 1, name = main} 耗时任务一
{number = 1, name = main} 耗时任务一
{number = 1, name = main} 耗时任务一
{number = 1, name = main} 耗时任务一
    */

所以,为了真正实现预期的效果,我们需要配合 groupenterleave 两个函数。每次执行 group.enter() 表示一个任务被加入到列队组 group 中,此时 group 中的任务的引用计数会加1,当使用 group.leave() ,表示 group 中的一个任务完成,group 中任务的引用计数减1.当 group 列队组里面的任务引用计数为0时,会通知 notify 函数,任务执行完成。

注意:enter()leave() 成对出现的。

    func enterLeaveGroup() {
        let group = DispatchGroup()
        let queue = DispatchQueue.global()
        
        // 把该任务添加到组队列中执行
        group.enter()
        queue.async(group: group, qos: .default, flags: []) {
            // 增加耗时
            DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
                 for _ in 0...4 {
                     print("\(Thread.current) 耗时任务一")
                 }
                // 执行完之后从组队列中移除
                group.leave()
            })
            
        }
        
        // 把该任务添加到组队列中执行
        group.enter()
        queue.async(group: group, qos: .default, flags: []) {
            for _ in 0...4 {
                print("\(Thread.current) 耗时任务二")
            }
            // 执行完之后从组队列中移除
            group.leave()
        }
        
        // 当上面所有的任务执行完之后通知
        group.notify(queue: queue) {
            print("\(Thread.current) 所有的任务执行完了")
        }
    }
    
    /* 输出结果
{number = 6, name = (null)} 耗时任务二
{number = 6, name = (null)} 耗时任务二
{number = 6, name = (null)} 耗时任务二
{number = 6, name = (null)} 耗时任务二
{number = 6, name = (null)} 耗时任务二
{number = 1, name = main} 耗时任务一
{number = 1, name = main} 耗时任务一
{number = 1, name = main} 耗时任务一
{number = 1, name = main} 耗时任务一
{number = 1, name = main} 耗时任务一
{number = 7, name = (null)} 所有的任务执行完了
    */

asyncAfter 延时处理

使用 asyncAfter 来提交任务进行延迟。之前是使用 dispatch_time,现在是使用 DispatchTime 对象表示。可以使用静态方法 now 获得当前时间,然后再通过加上 DispatchTimeInterval 枚举获得一个需要延迟的时间。注意:仅仅是用于在具体时间执行任务,不要在资源竞争的情况下使用。并且在主列队使用。

let delay = DispatchTime.now() + DispatchTimeInterval.seconds(10)
        DispatchQueue.main.asyncAfter(deadline: delay) {
            // 延迟执行
        }

因为在 DispatchTime 中自定义了“+”号, 我们进一步简化

public func +(time: DispatchTime, seconds: Double) -> DispatchTime

let delay = DispatchTime.now() + 10
        DispatchQueue.main.asyncAfter(deadline: delay) {
            // 延迟执行
        }

DispatchSemaphore 信号量

Semaphore 是保证线程安全的一种方式,而且继 OSSpinLock 不再安全后,Semaphore 似乎成为了最快的加锁的方式。

信号量在多线程开发中被广泛使用,当一个线程在进入一段关键代码之前,线程必须获取一个信号量,一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待前面的线程释放信号量。

信号量的具体做法是:当信号计数大于0时,每条进来的线程使计数减1,直到变为0,变为0后其他的线程将进不来,处于等待状态;执行完任务的线程释放信号,使计数加1,如此循环下去。

    func semaphore() {
        // 创建信号量,参数:信号量的初值,如果小于0则会返回 NULL, value 表示最多几个资源可访问
        let semaphore = DispatchSemaphore(value: 2)
        let queue = DispatchQueue.global()
        // 任务1
        queue.async {
            // 等待降低信号量
            semaphore.wait()
            print("运行任务1")
            sleep(1)
            print("结果任务1")
            // 提高信号量
            semaphore.signal()
        }
        // 任务2
        queue.async {
            // 等待降低信号量
            semaphore.wait()
            print("运行任务2")
            sleep(1)
            print("结果任务2")
            // 提高信号量
            semaphore.signal()
        }
        // 任务3
        queue.async {
            // 等待降低信号量
            semaphore.wait()
            print("运行任务3")
            sleep(1)
            print("结果任务3")
            // 提高信号量
            semaphore.signal()
        }
    }
    
    /* 输出结果:
运行任务2
运行任务1
结果任务1
结果任务2
运行任务3
结果任务3
    */

由于设定的信号值为2,先执行两个线程,等执行完一个,才会继续执行下一个,保证同一时间执行的线程数不超过2。

Barrier 屏障

GCD 里的 BarrierNSOperationQueuedependency 比较接近,C 任务开始之前需要 A 任务完成,或者 A 和 B 任务完成。

    func barrier() {
        let queue = DispatchQueue(label: "barrier", attributes: .concurrent)
        queue.async {
            print("任务A")
            
        }
        queue.async {
            sleep(3)
            print("任务B")
        }
        // 这里 barrier,必须等任务C完成后,才走后面任务D
        queue.async(flags: .barrier) {
            sleep(2)
            print("任务C")
        }
        queue.async {
            print("任务D")
        }
    }

假如我们有一个并发的列队用来读写一个数据对象,如果这个列队的操作是读,那么可以同时多个进行。如果有写的操作,则必须保证在执行写操作时,不会有读取的操作执行,必须等待写操作完成之后再开始读取操作,否则会造成读取的数据出错,经典的读写问题。这里我们就可以使用 barrier, 通过在并发代码中使用 barrier 将能够保证写操作在所有读取操作完成之后进行,而且确保写操作执行完成之后再开始后续的读取操作。

// 保证写入时,不能读数据
let item = DispatchWorkItem(qos: .default, flags: .barrier) {
            // write data
        }
        let dataQueue = DispatchQueue(label: "queue", attributes: .concurrent)
        dataQueue.async(execute: item)

Suspend / Resume

Suspend 可以挂起一个线程,即暂停线程,但是仍然暂用资源,只是不执行

Resume 回复线程,即继续执行挂起的线程。

循环执行任务

调用 concurrentPerform()

        // 并发执行5次
        DispatchQueue.concurrentPerform(iterations: 5) {
            print("\($0)")
        }

DispatchSource

DispatchSource提高了相关的API来监控低级别的系统对象,比如:Mach ports, Unix descriptors, Unix signals, VFS nodes。并且能够异步提交事件到派发列队执行。

简单定时器

func timer() {
        // 定时时间
        var timeCount = 60
        // 创建时间源
        let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global())
        // 每秒调用一次
        timer.schedule(deadline: .now(), repeating: .seconds(1))
        timer.setEventHandler {
            timeCount -= 1
            if timeCount <= 0 { timer.cancel() }
            DispatchQueue.main.async {
                print("update UI or other task")
            }
        }
        // 启动时间源
        timer.resume()
    }

注意事项
线程死锁
不要在主列队中执行同步任务,这样会造成死锁问题。

参考文章:

Apple Documentation
Swift4 - GCD的使用
iOS多线程 Swift4 GCD深入解析
GCD精讲(Swift 3&4)
iOS GCD中级篇 - dispatch_semaphore(信号量)的理解及使用

你可能感兴趣的:(Swift5 多线程 - GCD)