iOS 多线程 - Operation

2022-09-23 08:47:10 浏览数 (1)

前言

Hi Coder,我是 CoderStar!

我们之前已经讲过 iOS 多线程-Thread 以及 iOS 多线程-GCD,我们今天来聊一聊 iOS 多线程中最后一种比较常用的方式--Operation

概览

对于 Operation 而言,其相关的类比 GCD 要少的多。Operation本身是一个抽象类,不能直接进行使用,其定义了相关的方法及属性,需要靠子类进行相应的实现,系统已经实现了一个 --BlockOperation。(在 OC 中,还有一个是NSInvocationOperation,但在 Swift 中,该子类已经在 Swift4 里去掉,想必去掉的原因大家也很容易理解,因为 Swift 语言本身就不推荐 selector 这种形式)。

Operation 底层建立在GCD之上,是更高一级的抽象,使我们可以面向对象(Cocoa 对象)的方式进行多线程编程。

其实 NSOpertion 是先于GCD引进的,在当时,NSOperationQueue 接收 NSOperation 对象并创建一个线程,然后在该线程上运行 main方法 ,运行完成之后再杀死该线程。这种方式相对于后面出现的GCD底层的线程池而言,效率就很低,所以在 Mac OS 10.5 以及 iOS 2 开始便对NSOpertion底层在基于GCD的基础上进行完全重写,利用GCD的相关特性提高性能并提供了一些新功能。如果想简单佐证下,可以看到OperationQueue拥有一个unowned(unsafe) open var underlyingQueue: DispatchQueue?属性。

如果大家对 Operation 底层实现比较有兴趣,可以在开源的 Foundtion 框架中查看Operation.swift[1]

基本原理

先罗列一下OperationOperationQueue主要的属性及方法。

注释会有相应说明。

Operation

代码语言:javascript复制
// Operation

// MARK: - 属性

/// 下列几个属性为Operation的状态,只读属性

open var isReady: Bool { get }
open var isExecuting: Bool { get }
open var isCancelled: Bool { get }
open var isFinished: Bool { get }


/// 该属性已经被下列isAsynchronous属性替代
open var isConcurrent: Bool { get }

/// 用来标识当前操作是否为异步
/// 需要注意该属性是当直接调用start方法时才会生效,但添加Operation到OperationQueue,队列将忽略该属性的值
/// 默认值为false
@available(iOS 7.0, *)
open var isAsynchronous: Bool { get }


/// 操作优先级
/// 当队列中operation很多时,我们可以通过设置该属性来调整Operation在 同一队列中 的优先级,同时这个前提是Operation都是处于readey状态ati
open var queuePriority: Operation.QueuePriority

/// 该属性与Thread所拥有的服务质量等级属性一致
/// 主要用来描述任务在进程中整体的优先级
@available(iOS 8.0, *)
open var qualityOfService: QualityOfService

/// 任务完成后的回调方法
/// 当isFinished属性设置为YES时才会执行该回调
@available(iOS 4.0, *)
open var completionBlock: (() -> Void)?

// MARK: - 方法

/// 启动
/// 并发Operation时需要重写该方法
/// 可以不把operation加入到队列中,手动触发执行,与调用普通方法一样
open func start()

/// 非并发Operation需要重写该方法
open func main()

/// 取消操作
open func cancel()

/// 添加依赖
open func addDependency(_ op: Operation)

/// 移除依赖
open func removeDependency(_ op: Operation)

Operation几个属性、方法再进行详细的说明:

cancel方法 如果这个操作正在执行,调用 cancel() 只会将状态 isCanceled 置为 true,但不会影响操作的继续执行。如果操作还没执行,调用 cancel() 会将状态 isCanceledisReady 置为 true, 如果执行取消后的操作,会直接将状态 isFinished 置为 true 而不会执行操作。也会触发completionBlock方法。所以当我们子类化Operation的时候在处理耗时以及启动等相关操作时,应先检查isCanceled状态。

addDependency方法

  • 需要注意在设置时不要设置成循环依赖,比如 A 依赖 B、B 又依赖 A,这样会形成死锁,导致谁也不会执行。
  • 可以跨操作队列设置依赖。
  • 当给某个 Operation 添加依赖的 Operation 后,只有其所依赖的所有 Operation 都执行完毕,当前的 Operation 才能开始执行。不管依赖的 Operation 是执行成功了还是失败了,或者是取消了,都认为是执行完毕了。

OperationQueue

代码语言:javascript复制
// Operation

// MARK: - 属性

/// 最大并发操作数,也就是该队列中最多允许几个Operation在同时运行
open var maxConcurrentOperationCount: Int

// MARK: - 方法

/// 取消所有操作
open func cancelAllOperations()

/// 调用该方法会阻塞当前线程,等待所有任务完成之后才会执行后续逻辑
/// 和 DispatchGroup 一定条件下是类似的
open func waitUntilAllOperationsAreFinished()

/// 添加 Operation
open func addOperation(_ op: Operation)

/// 闭包形式添加 Operation
@available(iOS 4.0, *)
open func addOperation(_ block: @escaping () -> Void)

/// 类似 GCD 的栅栏函数
@available(iOS 13.0, *)
open func addBarrierBlock(_ barrier: @escaping () -> Void)

OperationQueue几个属性、方法再进行详细的说明:

maxConcurrentOperationCount属性

  • maxConcurrentOperationCount 如果不设置值时,默认值会取defaultMaxConcurrentOperationCount,也就是 -1,此时默认最大操作数由 OperationQueue 对象根据当前系统条件(系统内存与 CPU)动态确定。
  • maxConcurrentOperationCount 为 0 时,队列中的 Operation 不会执行。
  • maxConcurrentOperationCount 为 1 时,队列串行执行。
  • maxConcurrentOperationCount 大于 1 时,队列并发执行,当然这个值不应超过系统限制,即使自己设置一个很大的值,系统也会自动调整为 min(自己设定的值,系统设定的默认最大值),系统默认限制应该是 64

需要注意,因为有queuePriority的存在,同一个 QueueOperation 之间有优先级,所以先进入 Queue 的 Operation 不一定先运行,所以当maxConcurrentOperationCount设置为 1 时并不是一个真正意义上的串行队列,优先级较高后加入的 Operation 有可能会先执行。

64 这个值在 GCD 下应该也是默认最大线程数,但是可以调整目标队列的优先级进行调整。这里涉及到一个线程爆炸的概念,后面可能还会出一篇文章写这些东西。

从上面 Operation 的几个状态属性我们可以知道 Operation 在程序运行过程中状态会进行相应的流转,其状态图如下所示。

OperationState

使用

对于一般的任务,我们可以直接使用BlockOperation。使用示例如下:

代码语言:javascript复制
let operation = BlockOperation {
  // do something
}

// BlockOperation并不是只能执行一个Block,而是可以添加多个,内部会有一个数组进行存储。
operation.addExecutionBlock {
   // do something
}

let queue = OperationQueue()
queue.addOperation(operation)

但是很多时候,我们需要继承Operation进行一些自定义操作,如网络请求的依赖。这时,我们需要继承Operation重写对应的属性与方法来实现。

网络请求的依赖为何需要子类化 Operation:普通的Operation等待 main 方法执行完毕之后就会自动将isFinished置为true,继而执行下一个,但是对于网络请求这种场景,我们需要手动控制,等待网络请求回调之后再将isFinished置为true

这部分内容,Apple 的文档上有详细的介绍,Operation 文档链接[2]

除了将Operation放入OperationQueue运行这种方式之外,还可以直接调用start方法进行运行。

对于第一种方式,OperationQueue会自动为Operation开辟线程,不需进行额外的处理,对于第二种方式,就需要我们手动进行控制,我们可以将操作设计为同步或者异步的,也就是所谓的非并发Operation以及并发Operation

当然,其实直接调用start方法这种方式在日常开发过程中用的比较少的,主要是使用OperationQueue这种方式。下列部分主要是给大家拓宽一下 Operation 的使用方式以及了解当子类化 Operation 时我们需要注意的地方。

Operation 内部本身是线程安全的,当我们子类化 Operation 时,不管是非并发 Operation 还是并发 Operation,我们也需要保证其线程安全,所以需要在一些地方加上互斥锁,如后续操作中的状态切换时。

非并发 Operation

对于非并发 Operation,因为 Operation 在默认情况直接调用start方法是一个同步操作,所以当我们继承 Operation 来实现一个非并发 Operation 时,我们只需要重写main方法。

代码语言:javascript复制
class SyncOperation: Operation {
    override func main() {
        // do something
    }
}

并发 Operation

如果是并发 Operation,则至少需要重写以下属性及方法,并且运行状态更新时需要生成 KVO 通知。

  • isAsynchronous
  • isExecuting
  • isFinished
  • start()

具体代码如下,请注意阅读注释:

代码语言:javascript复制
public class AsyncOperation: Operation {
    private var block: ((_ operation: AsyncOperation) -> Void)?

    private let queue = DispatchQueue(label: "async.operation.queue")
    private let lock = NSLock()

    private var _executing = false
    private var _finished = false

    /// 数据
    ///
    /// 为Operation绑定一下数据,方便被依赖的Operation获取该Operation处理后的一些数据
    public var data: Any?

    /// 是否执行
    ///
    /// 内部加锁保证线程安全
    public override var isExecuting: Bool {
        get {
            lock.lock()
            let wasExecuting = _executing
            lock.unlock()
            return wasExecuting
        }
        set {
            if isExecuting != newValue {
                willChangeValue(forKey: "isExecuting")
                lock.lock()
                _executing = newValue
                lock.unlock()
                didChangeValue(forKey: "isExecuting")
            }
        }
    }

    /// 是否结束
    ///
    /// 内部加锁保证线程安全
    /// 需要手动进行KVO,否则completionBlock不会被触发,被依赖的Operation也不会开始
    public override var isFinished: Bool {
        get {
            lock.lock()
            let wasFinished = _finished
            lock.unlock()
            return wasFinished
        }
        set {
            if isFinished != newValue {
                willChangeValue(forKey: "isFinished")
                lock.lock()
                _finished = newValue
                lock.unlock()
                didChangeValue(forKey: "isFinished")
            }
        }
    }

    /// 标识该Operation是否以异步形式运行
    public override var isAsynchronous: Bool {
        return true
    }

    public override func start() {
        /// 启动之前先判断是否已取消,防止浪费操作
        if isCancelled {
            isFinished = true
            return
        }

        isExecuting = true

        queue.async { [weak self] in
            self?.main()
        }
    }

    public override func main() {
        if let block = block {
            block(self)
        } else {
            finish()
        }
    }
}

// MARK: - 公开方法

extension AsyncOperation {
    public convenience init(block: ((_ operation: AsyncOperation) -> Void)?) {
        self.init()
        self.block = block
    }

    /// 完成任务
    ///
    /// 完成一些异步操作后再手动调用完成 Operation
    public func finish() {
        isExecuting = false
        isFinished = true
    }
}

代码中几个地方需要特别说明一下:

  1. 状态变量切换时,为保证线程安全,我们需要进行加锁;
  2. 虽然官方文档说main方法不需要强制进行重写,但为了逻辑性,start方法主要负责任务的启动,main方法中进行任务的处理,所以重写的main方法。
  3. 关于isAsynchronous属性,刚开始我以为其可以控制Operation是否自动开辟线程,但是根据实验以及查看源码之后,发现其应该只是一个标识当前Operation是否是异步操作的一个标志而已,当设置为 true 时,我们需要自己开辟线程进行任务的分发。当我们确定该Operation后续都是以OperationQueue的形式运行,我们也可以将isAsynchronous返回值改为 false,去除内部的队列。

关于 Operation的具体应用,大家可以去阅读Alamofire或者SDWebImage等开源库的源码,内部都有其应用。

GCD VS Operation

使用 GCD 还是使用 Operation 这个问题其实在社区已经争论了很久,从斯坦福大学的 CS193p 课程推荐使用 GCD,到 WWDC 2012 时演讲者推荐使用Operation,也能看出开发者对该问题的看法不一致,该节我们主要来聊一聊两者各自优势以及差别。

目前网络上的很多文章都是基于没有DispatchWorkItem对象前提下对 GCD 和Operation做的对比,大家阅读时需要注意一下。

1、从两者所在层次来讲:GCD 底层是 C 语言的 API,而 Operation 是 GCD 基础上更高层次的抽象,那 GCD 相对 Operation 来说肯定是又快又轻的。(Operation 在使用 GCD API 的基础上还会加上一些锁用来保证线程安全)

但是反过来说因为 Operation 是更高层次的抽象,按照一般的经验法则来看,我们应首先使用最高级别的 API,然后在根据需要完成内容进行降级。从这一角度来看,使用 Operation 抽象度更高,更符合面向对象的思想,也有利于底层的无痕变更。

2、从两者提供的 API 来讲:其实 GCD 和 Operation 两者之间是很相似的,特别是当DispatchWorkItem对象(@available(macOS 10.10, iOS 8.0, *))出来之后,从一定意义上讲,DispatchWorkItem可以类比到Operation对象,DispatchQueue可以类比到OperationQueue对象。比如DispatchWorkItemOperation对象都可以进行cancel等操作,DispatchQueueOperationQueue对象都可以添加任务或操作(对象以及闭包两种形式),栅栏函数,进行挂起以及恢复(前者是两个对应方法,后者是一个属性)等。但两者还是有一些区别的,比如:

  • OperationQueue可以设置并发操作的最大数量maxConcurrentOperationCount。在一定条件下可以类比到 GCD 的信号量
  • 在不同的任务之间建立依赖关系addOperation;在一定条件下可以类比到 GCD 的 DispatchWorkItempublic func notify(queue: DispatchQueue, execute: DispatchWorkItem) 方法
  • OperationQueue可以取消队列中的所有操作。
  • ...

3、相对 GCD 而言,Operation可以通过子类化的方式将一些操作内聚,更方便管理。

最后

要更加努力呀!

Let's be CoderStar!

推荐学习资料

  • NSOperation vs Grand Central Dispatch[3]
  • guide-to-blocks-grand-central-dispatch[4]
  • When to use NSOperation vs. GCD[5]
  • Operation and OperationQueue Tutorial in Swift[6]
  • Advanced NSOperations[7]

参考资料

[1]Operation.swift: https://github.com/apple/swift-corelibs-foundation/blob/main/Sources/Foundation/Operation.swift

[2]Operation 文档链接: https://developer.apple.com/documentation/foundation/operation

[3]NSOperation vs Grand Central Dispatch: https://stackoverflow.com/questions/10373331/nsoperation-vs-grand-central-dispatch/10375616#10375616

[4]guide-to-blocks-grand-central-dispatch: https://cocoasamurai.blogspot.com/2009/09/guide-to-blocks-grand-central-dispatch.html

[5]When to use NSOperation vs. GCD: http://eschatologist.net/blog/?p=232

[6]Operation and OperationQueue Tutorial in Swift: https://www.raywenderlich.com/5293-operation-and-operationqueue-tutorial-in-swift

[7]Advanced NSOperations: https://developer.apple.com/videos/play/wwdc2015/226/

0 人点赞