SwiftNote-多线程

433 阅读9分钟

前言

多线程可以做什么?

  • 网络请求
  • 文件 IO
  • 复杂计算
  • 数据模型转换...

Swift 中的多线程编程方式:

  1. Thread
  2. Cocoa Operation(Operation 和 OperationQueue)
  3. Grand Central Dispatch (GCD)

1. Thread 创建与使用

Thread 的快捷创建方式:

  • detachNewThread(_ block: @escaping() -> Void)

    func threadTest() {
        for i in 0..<10 {
            Thread.detachNewThread {
                print(i)
            }
        }
    }
    // 打印结果:0 1 2 4 5 6 7 8 9 3(乱序的)
    
  • detachNewThreadSelector(_ selector: Selector, toTarget target:Any, with argument: Any?)

    Thread.detachNewThreadSelector(#selector(threadSelectorTest), toTarget: self, with: nil)
    
    @objc func threadSelectorTest() {
        print("thread Selector Test")
    }
    

Thread 的初始化器: 使用Thread(target:, selector:, object)来初始化一个 Thread,然后调用 start() 函数启动线程。

class ObjectForThread {
    
    func threadTest() {
        let thread = Thread(target: self, selector: #selector(threadWorker), object: nil)
        thread.start()
    }
    
    @objc func threadWorker() {
        print("threadWorker")
    }
}

let t = ObjectForThread()
t.threadTest()

2. Operation 和 OperationQueue

Operation 是面向对象的多线程技术,需要配合 OperationQueue 一起使用,Operation 可以取消、设置依赖关系。可以完成任务优先级的其他复杂逻辑,当然也可以子类化。

2.1 Operation 简介

在 Swift 中 Operation 有两种:

  • Operation

    class MyOperation: Operation {
        override func main() {
            sleep(1)
            print("this is my operation")
        }
    }
    
    let operation = MyOperation()
    let queue = OperationQueue()
    queue.addOperation(operation)
    print("operationTest")
    // operationTest
    // this is my operation
    
  • BlockOperation

    let operation = BlockOperation {
        [weak self] in
        self?.threadWorker()
        return
    }
    
    func threadWorker() {
        print("threadWorker")
    }
    
    let queue = OperationQueue()
    queue.addOperation(operation)
    print("operationTest")
    // operationTest
    // threadWorker
    

Operation 的状态:

  1. isReady: 是否准备好了可以执行
  2. isExecuting: 是否正在执行中
  3. isFinished: 是否完成了
  4. isCancelled: 是否取消了

Operation 有两种执行方式:同步(sync)和异步(async),同步的话需要重写其 main() 方法,异步的话需要重写其 start() 方法, 并且注意 isAsynchronousisExecutingisFinishedisCancelled 的状态。

Operation 还有一个完成的回调为:completionBlock, 其实对于同步执行的 Operation 来说,completionBlock 存在的意义不大,我们完全可以在重写 main() 函数的函数体里面等执行完我们的任务的时候再通知外界执行完毕,但是这个回调对于异步执行的 Operation 很重要,可以在这个回掉中获得执行完毕的通知。

class MyOperation: Operation {
    override func main() {
        sleep(1)
        print("this is my operation")
    }
}

let operation = MyOperation()
operation.completionBlock = { () -> Void in
    print("completionBlock")
}

let queue = OperationQueue()
queue.addOperation(operation)
print("operationTest")
// operationTest
// this is my operation
// completionBlock
2.2 OperationQueue 简介
  • OperationQueue 队列里可以加入很多个 Operation,可以把 OperationQueue 看作一个线程池,可往线程池中添加操作(Operation)到队列中。
  • 底层使用的是 GCD。
  • maxConcurrentOperationCount 可以设置最大并发数。
  • defaultMaxConcurrentOperationCount 根据当前系统条件确定的最大并发数。
  • 可以取消所有 Operation,但是当前正在执行的不会取消
  • 当所有的 Operation 执行完毕后 OperationQueue 会退出并销毁。

3. GCD

3.1 GCD 简介

GCD 的特点:

  • 任务 + 队列
  • 易用性
  • 效率高
  • 性能好

GCD 的队列:

  • 主队列:任务在主线程执行
  • 并行队列:任务会以先进先出的顺序入列和出列,但是因为同一时刻多个任务并发执行,所有任务的执行顺序不一定。
  • 串行队列:任务会以先进先出的顺序入列和出列,但是同一时刻只会执行一个任务。

GCD 的队列API:

  • Dispatch.main: 主队列
  • Dispatch.global: 全局队列
  • DispatchQueue(label:,qos:,attributes:,autoreleaseFrequency:,target:): 队列的初始化器
  • queue.label: 队列的标签,调试时使用
  • setTarget(queue: DispatchQueue?): 设置目标队列

补充说明:

DispatchQoS: 全局并发队优先级


老版(swift3之前)gcd有四种:highdefaultlowbackground, 优先级为:high>default>low>background

新版(swift3之后)gcd有六种:backgroundutilitydefaultuserInitiateduserInteractiveunspecified 优先级为:userInteractive>default>unspecified>userInitiated>utility>background


DispatchQueue.Attributes 队列的选项集合(option sets),包含两个选项:

  • concurrent:标识队列为并行队列。
  • initiallyInactive:标识运行队列中的任务需要手动触发,由队列的 activate 方法进行触发。如果未添加此标识,向队列中添加的任务会自动运行。

如果不设置该值,则表示创建串行队列。如果希望创建并行队列,并且需要手动触发,则该值需要设置为 [.concurrent, .initiallyInactive]


DispatchQueue.AutoreleaseFrequency: autoreleaseFrequency 的类型为枚举(enum),用来设置负责管理任务内对象生命周期的 autorelease pool 的自动释放频率。包含三个类型:

  • inherit:继承目标队列的该属性,
  • workItem:跟随每个任务的执行周期进行自动创建和释放
  • never:不会自动创建 autorelease pool,需要手动管理。

一般采用 workItem 行了。如果任务内需要大量重复的创建对象,可以使用 never 类型,来手动创建 aotorelease pool。


target: 这个参数设置了队列的目标队列,即队列中的任务运行时实际所在的队列。目标队列最终约束了队列的优先级等属性。 在程序中手动创建的队列最后都指向了系统自带的主队列全局并发队列

3.2 GCD 基本操作

  • sync: 同步执行,提交任务到当前队列里,并且直到任务执行完成,当前队列才会返回。

    let queue = DispatchQueue(label: "hyQueue", qos: .default, attributes: .concurrent, autoreleaseFrequency: .inherit, target: nil)
    // 同步执行
    queue.sync {
        print("in queue sync")
    } 
    print("after invoke queue sync method")
    // 执行结果:
    // in queue sync
    // after invoke queue sync method
    
  • async: 异步执行,调度一个任务去立即执行,但是不用等任务执行完毕当前队列就会返回。

    let queue = DispatchQueue(label: "hyQueue", qos: .default, attributes: .concurrent, autoreleaseFrequency: .inherit, target: nil)
    // 异步执行
    queue.async {
        sleep(2)
        print("in queue async")
    }
    print("after invoke queue method")
    // 执行结果:
    // after invoke queue method
    // in queue async
    
  • asyncAfter: 延时异步执行,调度一个任务多久去执行,但是不用等任务执行完毕当前队列就会返回。

    let queue = DispatchQueue(label: "hyQueue", qos: .default, attributes: .concurrent, autoreleaseFrequency: .inherit, target: nil)
    // 延时 2 秒执行
    queue.asyncAfter(deadline: .now() + 2) {
        print("in queue asyncAfter")
    }
    print("after invoke queue method")
    // 执行结果:
    // after invoke queue method
    // in queue asyncAfter
    

3.3 GCD 高级特性

3.3.1 DispatchGroup

DispatchGroup

DispatchGroup 的 wait 使用示例:

let workQueue = DispatchQueue.global()
let workGroup = DispatchGroup()
    
workGroup.enter()
workQueue.async {
    Thread.sleep(forTimeInterval: 1)
    print("请求 A 接口数据完成")
    workGroup.leave()
}
    
workGroup.enter()
workQueue.async {
    Thread.sleep(forTimeInterval: 2)
    print("请求 B 接口数据完成")
    workGroup.leave()
}
    
print("我是最开始执行的, 异步打印后续执行")
workGroup.wait()
print("整合接口 A 和 接口 B 的数据, 刷新页面!")
// 执行结果:
// 我是最开始执行的, 异步打印后续执行
// 请求 A 接口数据完成
// 请求 B 接口数据完成
// 整合接口 A 和 接口 B 的数据, 刷新页面!

DispatchGroup 的 notify 使用示例:

let workQueue = DispatchQueue.global()
let workGroup = DispatchGroup()
    
workGroup.enter()
workQueue.async {
    Thread.sleep(forTimeInterval: 1)
    print("请求 A 接口数据完成")
    workGroup.leave()
}
    
workGroup.enter()
workQueue.async {
    Thread.sleep(forTimeInterval: 2)
    print("请求 B 接口数据完成")
    workGroup.leave()
}
    
print("我是最开始执行的, 异步打印后续执行")
workGroup.notify(queue: workQueue) {
    print("整合接口 A 和 接口 B 的数据, 刷新页面!")
}
print("验证不堵塞线程")
// 执行结果:
// 我是最开始执行的, 异步打印后续执行
// 验证不堵塞线程
// 请求 A 接口数据完成
// 请求 B 接口数据完成
// 整合接口 A 和 接口 B 的数据, 刷新页面!
3.3.2 DispatchSource

简单来说,DispatchSource 是一个监视某些类型事件的对象。当这些事件发生时,它自动将一个 task 放入一个 DispatchQueue 的执行例程中。

DispatchSource 的类型有以下几种:

  • Mach port send right state changes
  • Mach port receive right state changes
  • External process state change
  • file descriptor ready for read
  • file descriptor ready for write
  • Filesystem node event
  • POSIX signal
  • Custom timer
  • Custom event

下面以 timer 举例说明如何使用:

var seconds = 5
let timer: DispatchSourceTimer = DispatchSource.makeTimerSource(flags: [], queue: DispatchQueue.global())
timer.schedule(deadline: .now(), repeating: 1)
timer.setEventHandler {
    seconds -= 1
    if seconds < 0 {
        timer.cancel()
    } else {
        print(seconds)
    }
}
timer.resume()
// 执行结果:
// 4 3 2 1 0

4. 多线程应用场景

  • 一个页面有三个网络请求,需要在三个网络请求都返回数据的时候刷新页面。(思路:使用 DispatchGroup)
  • 实现一个线程安全的 Array 的读和写。(思路:加锁,使用并发队列完成读写操作)
  • 编写一个多线程下载器,可以执行多个下载任务,每个任务可以保存当前下载字节数,总字节数,可以设置回调得到当前的下载进度。(思路: 使用 Operation 和 OperationQueue,子类化 Operation 来完成自己的内部业务逻辑)
  • 需要在主线程等待一个异步任务返回,才能继续执行下面的逻辑,但是又不希望堵塞用户交互事件。(思路:使用并发队列异步执行任务,完毕后通知主队列刷新 UI)

5. 多线程编程模式

  • Promise
  • Pipeline
  • Master-Slave
  • Serial Thread Confinement

5.1 Promise

所谓的 Promise 就是一个对象用来传递异步操作的消息。它代表了某个未来才会知道结果的事件(通常是一个异步操作),并且这个事件提供统一的 API,可以进一步处理。

Promise 对象的状态不受外界影响。Promise 对象代表一个异步操作,有三种状态:Pending(进行中,又称为 Incomplete)、Resolved(已完成,又称 Fulfilled)和Rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。

一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise 对象的状态改变,只有两种可能:从 Pending 变为 Resolved 或 从 Pending 变为 Rejected。只要这两种情况之一发生了,状态就凝固了,不会再改变了,会一直保持这个结果

普通的编程模式:

普通的编程模式

Promise 的编程模式:

Promise模式

5.2 Pipeline

  • Pipeline 是将一个任务分解为若干个阶段(Stage),前阶段的输出作为下阶段的输入,各个阶段有不同的工作者线程负责执行。
  • 多个任务的各个阶段是并行(Parallel)处理的。
  • 具体任务的处理是串行的,即完成一个任务要依次执行各个阶段,但从整体任务来看,不同任务的各个阶段的执行是并行的。

Pipeline 的工作流程:

Pipeline

5.3 Master-Slave

Master-Slave 是将一个任务分解为若干个语义等同的子任务,并由专门的工作者线程来并行执行这些子任务,既提高了计算效率,又实现了信息隐藏。

Master-Slave 的工作流程:

Master-Slave

5.4 Serial Thread Confinement

如果并发任务的执行涉及某个非线程安全的对象,而很多时候我们又不希望因此而引入锁(担心死锁、优先级翻转或其他不同线程调度资源而出现问题)。这时候可以通过将多个并发的任务存入队列实现任务的串行化,并未这些串行化的任务创建为宜的工作者线程进行处理。