iOS 多线程开发之 Operation

1,156 阅读13分钟

iOS 多线程开发之系列文章

iOS 多线程开发之概念

iOS 多线程开发之 Thread

iOS 多线程开发之 GCD

iOS 多线程开发之 Operation

iOS 多线程开发之线程安全

多线程开发是日常开发任务中不可缺少的一部分,在 iOS 开发中常用到的多线程开发技术有 GCD、OperationThread,本文主要讲解多线系列文章中关于 Operation 的相关知识和使用详解。

简介

Operation 是苹果公司提供的一套完整的多线程解决方案,实际上它是基于 GCD 更高一层的封装,完全面向对象。相对于 GCD 而言使用更加的简单、代码更具可读性。包括网络请求、图片压缩在内的诸多多线程任务案例都很好的使用了 Operation。当然 Operation 还需要 OperationQueue 这一重要角色配合使用。其中 Operation 是个抽象类,使用它必须用它的子类,可以实现它或者使用它定义好的子类:BlockOperation。创建 Operation 子类的对象,把对象添加到 OperationQueue 队列里执行。

BlockOperation

单任务

let op = BlockOperation{
    print("单任务:(Thread.current)")
}
op.start()

运行结果:

单任务:<NSThread: 0x6000008e03c0>{number = 1, name = main}
let op = BlockOperation()
op.addExecutionBlock {
    print("任务一:(Thread.current)")
}
op.start()

运行结果:

任务一:<NSThread: 0x600002e28080>{number = 1, name = main}

总结:
在主线程中单独使用 BlockOperation 执行一个操作的情况下,操作是在当前线程执行的,并没有开启新线程。

多任务

private func testOperationBlock() {

    let op = BlockOperation()

    ///任务一
    op.addExecutionBlock {
        print("任务一:\(Thread.current)")
    }

    ///任务二
    op.addExecutionBlock {
        print("任务二:\(Thread.current)")
    }

    ///任务三
    op.addExecutionBlock {
        print("任务三:\(Thread.current)")
    }
    op.start()
    print("结束")
}

结果:

任务二:<NSThread: 0x600003ee8900>{number = 6, name = (null)}
任务一:<NSThread: 0x600003ead6c0>{number = 7, name = (null)}
任务三:<NSThread: 0x600003ea4900>{number = 1, name = main}
结束

总结:

  1. BlockOperation 还提供了一个方法 addExecutionBlock,通过 addExecutionBlock: 就可以为 BlockOperation 添加额外的操作。这些操作(包括 blockOperationWithBlock 中的操作)可以在不同的线程中同时(并发)执行。只有当所有相关的操作已经完成执行时,才视为完成。
  2. 使用子类 BlockOperation,并调用方法 AddExecutionBlock: 的情况下,blockOperationWithBlock:方法中的操作 和 addExecutionBlock: 中的操作是在不同的线程中异步执行的。而且,这次执行结果中 blockOperationWithBlock:方法中的操作也不是在当前线程(主线程)中执行的。从而印证了blockOperationWithBlock: 中的操作也可能会在其他线程(非当前线程)中执行。
  3. 一般情况下,如果一个 BlockOperation 对象封装了多个操作。BlockOperation 是否开启新线程,取决于操作的个数。如果添加的操作的个数多,就会自动开启新线程。当然开启的线程数是由系统来决定的。

自定义 Operation

非并发 Operation

对于非并发的操作,在子类的 Operation 中,只需要复写 main() 就可以了。
在 main() 方法中,写一些任务执行的代码 等,另外在子类中可能还需要写一些初始化方法,以及一些访问读取数据的方法等等。
至于 Operation 的几种状态,我们是不需要关心的,当 main() 方法执行完毕,即 Operation 任务结束。
Operation 类中还提供了 cancel() 的方法,所以在 Operation 执行的时候需要判断是否已经取消了,因为取消操作可能在开始之前就执行了,也可能在任务执行过程中,所以代码中需要加入 isCancelled 的判断。

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        let op = MyOpertaion(data: "自定义Operation")
        op.start()
    }
}

class NonConcurrentOperation: Operation {
    var data: Any?
    init(data: Any) {
        super.init()
        self.data = data
    }
    
    override func main() {
        var isDone = false
        while !isCancelled && !isDone {
            // 在此方法中做一些相关业务操作等,并在完成后将isDone设置为true
            print("自定义Operation:\(Thread.current)")
            // ......
            isDone = true
        }
    }
}

运行结果:

自定义Operation:<NSThread: 0x6000030781c0>{number = 1, name = main}

并发 Operation

Operation 对象默认以同步方式执行,也就是说,在调用 start() 方法的线程中执行任务。但是,由于 OperationQueue 为非并发操作提供线程,所以大多数 Operation 仍然是异步运行的。但是,如果我们手动使用 Operation,不用 OperationQueue,并且仍然希望它们异步运行,则必须采取适当的操作来确保能够做到这一点。我们可以通过将 Operation 对象定义为并发操作来实现这一点。至于并发的 Operation,稍微有些复杂了,因为这里面的状态需要开发人员来管控。

创建一个并发的 Operation,则至少需要复写以下的方法和属性:

  • start()
  • isAsynchronous
  • isExecuting
  • isFinished
import Foundation

public class ConcurrentOperation: Operation {

    public var completedBlock: (() -> Void)?

    public override var isExecuting: Bool{
        return _executing
    }

    public override var isFinished: Bool{
        return _finished
    }
    
    public override var isAsynchronous: Bool{
        return true
    }

    // MARK: -

    // MARK: 利用 KVO 来通知 Operation 的 isExecuting(是否正在进行中),以及 isFinished(是否已经完成)

    //指定用于记录任务是否执行
    private var _executing:Bool = false{
        // kvo isExecuting
        willSet{
            willChangeValue(forKey: ModifyState.isExecuting.rawValue)
        }
        didSet{
            didChangeValue(forKey: ModifyState.isExecuting.rawValue)
        }
    }

    // 指定用于记录任务是否完成
    private var _finished:Bool = false{
        // kvo isFinished
        willSet{
            willChangeValue(forKey: ModifyState.isFinished.rawValue)
        }
        didSet{
            didChangeValue(forKey: ModifyState.isFinished.rawValue)
        }
    }
    
    // MARK: -

    /// 修改状态枚举(重写状态的字段标识)
    private enum ModifyState: String{
        case isExecuting = "isExecuting"
        case isFinished = "isFinished"

    }
    
    // MARK: 重写 start() 方法,开辟新的线程执行需要的耗时工作。
    // MARK: 注意! 不能调用父类的 start() 方法。
    // MARK: 因为 Operation 有 cancel() 方法来取消操作,而且我们并不知道在何时取消,所以我们需要在几个地方注意是否取消了操作.
    public override func start() {
        // 检测取消状态
        if isCancelled {
            done()
            return
        }
        // 修改状态 -> 执行
        _executing = true
        // 开启任务->并行,完成回调测试
        startTask()
    }

    // 开启任务(模拟耗时任务)
    private func startTask(){
        DispatchQueue.global().async { [weak self] in
            print("线程:",Thread.current)
            // 耗时
            sleep(2)
            for i in 0...2{
                print("\(i)")
            }

            // 检测状态
            if self?.isCancelled ?? false{
                self?.done()
                return
            }

            DispatchQueue.main.async { [weak self] in
                // 完成
                self?.completedBlock?()
                self?.done()
            }
        }
    }

    // 重写取消
    public override func cancel() {
        // 加锁保证线程安全
        objc_sync_enter(self)
        done()
        objc_sync_exit(self)
    }
    
    // 自定义 cancel
    private func done() {
        super.cancel()
        print("done start",isCancelled)
        if(_executing) {
            _finished = true
            _executing = false
        }
        print("done end",isCancelled)
    }
}

创建队列

OperationQueue 一共有主队列、自定义队列两种队列。其中自定义队列同时包含了串行、并发功能。下边是主队列、自定义队列的基本创建方法和特点。

主队列

凡是添加到主队列中的操作,都会放到主线程中执行(注:不包括操作使用addExecutionBlock:添加的额外操作,额外操作可能在其他线程执行)。

主队列获取方法:

let mainQueue = OperationQueue.main

示例:

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        let mainQueue = OperationQueue.main
        let operation1 = BlockOperation {
            print("任务一:\(Thread.current)")
        }
        
        let opertaion2 = BlockOperation {
            print("任务二:\(Thread.current)")
        }
        
        let opertaion3 = BlockOperation {
            print("任务三:\(Thread.current)")
        }
        
        let opertaion4 = BlockOperation {
            print("任务四:\(Thread.current)")
        }
        
        mainQueue.addOperation(operation1)
        mainQueue.addOperation(opertaion2)
        mainQueue.addOperation(opertaion3)
        mainQueue.addOperation(opertaion4)
    }
}

运行结果:

任务一:<NSThread: 0x6000009bc280>{number = 1, name = main}
任务二:<NSThread: 0x6000009bc280>{number = 1, name = main}
任务三:<NSThread: 0x6000009bc280>{number = 1, name = main}
任务四:<NSThread: 0x6000009bc280>{number = 1, name = main}

自定义队列

添加到这种队列中的操作,就会自动放到子线程中执行。

自定义队列创建方法

let queue = OperationQueue()

示例:

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let queue = OperationQueue()
        // 最大并发数
        queue.maxConcurrentOperationCount = 4
        let operation1 = BlockOperation {
            print("任务一 = \(Thread.current)")
        }
        let operation2 = BlockOperation {
            print("任务二 = \(Thread.current)")
        }
        let operation3 = BlockOperation {
            print("任务三 = \(Thread.current)")
        }
        let operation4 = BlockOperation {
            print("任务四 = \(Thread.current)")
        }
        queue.addOperation(operation1)
        queue.addOperation(operation2)
        queue.addOperation(operation3)
        queue.addOperation(operation4)
    }
}

运行结果:

任务三 = <NSThread: 0x600000bd6980>{number = 4, name = (null)}
任务二 = <NSThread: 0x600000ba1dc0>{number = 7, name = (null)}
任务一 = <NSThread: 0x600000bf6500>{number = 6, name = (null)}
任务四 = <NSThread: 0x600000bf7100>{number = 5, name = (null)}

OperationQueue 控制串行执行、并发执行

最大并发操作数:maxConcurrentOperationCount

  • maxConcurrentOperationCount 默认情况下为 -1,表示不进行限制,可进行并发执行。
  • maxConcurrentOperationCount 等于 1 时,队列为串行队列。只能串行执行。
  • maxConcurrentOperationCount 大于 1 时,队列为并发队列。操作并发执行,当然这个值不应超过系统限制,即使自己设置一个很大的值,系统也会自动调整为 min {自己设定的值,系统设定的默认最大值}。

maxConcurrentOperationCount = 1 时

示例:

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        print("start")
        let queue = OperationQueue()
        queue.maxConcurrentOperationCount = 1; // 串行队列
        queue.addOperation {
            sleep(2)
            print("0---\(Date())\(Thread.current)")
        }
        queue.addOperation {
            sleep(2)
            print("1---\(Date())\(Thread.current)")
        }
        let operation1 = BlockOperation.init {
            sleep(2)
            print("2---\(Date())\(Thread.current)")
        }
        operation1.addExecutionBlock {
            sleep(2)
            print("3---\(Date())\(Thread.current)")
        }
        queue.addOperation(operation1)
        print("end")
    }
}

运行结果:

start
end
0---2021-07-21 08:30:46 +0000<NSThread: 0x600003f23140>{number = 6, name = (null)}
1---2021-07-21 08:30:48 +0000<NSThread: 0x600003f2d540>{number = 5, name = (null)}
2---2021-07-21 08:30:50 +0000<NSThread: 0x600003f23140>{number = 6, name = (null)}
3---2021-07-21 08:30:50 +0000<NSThread: 0x600003f2d540>{number = 5, name = (null)}

maxConcurrentOperationCount > 1 时

示例:

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        print("start")
        let queue = OperationQueue()
        queue.maxConcurrentOperationCount = 4; // 串行队列
        queue.addOperation {
            sleep(2)
            print("0---\(Date())\(Thread.current)")
        }
        queue.addOperation {
            sleep(2)
            print("1---\(Date())\(Thread.current)")
        }
        let operation1 = BlockOperation.init {
            sleep(2)
            print("2---\(Date())\(Thread.current)")
        }
        operation1.addExecutionBlock {
            sleep(2)
            print("3---\(Date())\(Thread.current)")
        }
        queue.addOperation(operation1)
        print("end")
    }
}

运行结果:

start
end
0---2021-07-21 08:33:23 +0000<NSThread: 0x600001aa89c0>{number = 2, name = (null)}
1---2021-07-21 08:33:23 +0000<NSThread: 0x600001aace00>{number = 6, name = (null)}
2---2021-07-21 08:33:23 +0000<NSThread: 0x600001aa07c0>{number = 4, name = (null)}
3---2021-07-21 08:33:23 +0000<NSThread: 0x600001aa85c0>{number = 3, name = (null)}

Operation 操作依赖

Operation、OperationQueue 最吸引人的地方是它能添加操作之间的依赖关系。通过操作依赖,我们可以很方便的控制操作之间的执行先后顺序。Operation 提供了 3 个接口供我们管理和查看依赖。

添加依赖

func addDependency(_ op: Operation); 添加依赖,使当前操作依赖于操作 op 的完成。

示例:

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        testDependency()
    }
    
    //测试op依赖关系
    //A,B - C
    //C,D - E
    func testDependency(){
        let opA = BlockOperation()
        let opB = BlockOperation()
        let opC = BlockOperation()
        let opD = BlockOperation()
        let opE = BlockOperation()
        ///创建任务
        opA.addExecutionBlock {
            for i in 0...10{
                if i == 10{
                    print("A--\(i)")
                }
            }
        }
        
        opB.addExecutionBlock {
            for i in 0...10{
                if i == 10{
                    print("B--\(i)")
                }
            }
        }
        
        opC.addExecutionBlock {
            for i in 0...10{
                if i == 10{
                    print("C--\(i)")
                }
            }
        }
        
        opD.addExecutionBlock {
            for i in 0...10{
                if i == 10{
                    print("D--\(i)")
                }
            }
            
        }
        
        opE.addExecutionBlock {
            for i in 0...10{
                if i == 10{
                    print("E--\(i)")
                }
            }
        }
        
        ///添加依赖
        opC.addDependency(opA)
        opC.addDependency(opB)
        opE.addDependency(opC)
        opE.addDependency(opD)
        let queue = OperationQueue()
        queue.maxConcurrentOperationCount = 6
        queue.addOperations([opA,opB,opC,opD,opE], waitUntilFinished: false)
        print("end")
    }
}

移除依赖

func removeDependency(_ op: Operation); 移除依赖,取消当前操作对操作 op 的依赖。

获取依赖

var dependencies: [Operation] { get }; 在当前操作开始执行之前完成执行的所有操作对象数组。

小心死锁: 如果Operation之间互相依赖,比如队列 A 中的 Operation1 依赖Operation2, 而 Operation2 依赖 Operation3Operation3 依赖 Operation1, 这就会陷入互相等待的死锁。

Operation 优先级

Operation 提供了 queuePriority(优先级)属性,queuePriority 属性适用于同一操作队列中的操作,不适用于不同操作队列中的操作。默认情况下,所有新创建的操作对象优先级都是 normal。但是我们可以通过 setQueuePriority: 方法来改变当前操作在同一队列中的执行优先级。

优先级的取值:

public enum QueuePriority : Int {
  case veryLow = -8
  case low = -4
  case normal = 0
  case high = 4
  case veryHigh = 8
}

上边我们说过:对于添加到队列中的操作,首先进入准备就绪的状态(就绪状态取决于操作之间的依赖关系),然后进入就绪状态的操作的开始执行顺序(非结束执行顺序)由操作之间相对的优先级决定(优先级是操作对象自身的属性)。 

那么,什么样的操作才是进入就绪状态的操作呢?
当一个操作的所有依赖都已经完成时,操作对象通常会进入准备就绪状态,等待执行。

举个例子,现在有 4 个优先级都是 normal(默认级别)的操作:op1,op2,op3,op4。其中 op3 依赖于 op2,op2 依赖于 op1,即 op3 -> op2 -> op1。现在将这 4 个操作添加到队列中并发执行。

  • 因为 op1 和 op4 都没有需要依赖的操作,所以在 op1,op4 执行之前,就是处于准备就绪状态的操作。

  • 而 op3 和 op2 都有依赖的操作(op3 依赖于 op2,op2 依赖于 op1),所以 op3 和 op2 都不是准备就绪状态下的操作。理解了进入就绪状态的操作,那么我们就理解了queuePriority 属性的作用对象。

  • queuePriority 属性决定了进入准备就绪状态下的操作之间的开始执行顺序。并且,优先级不能取代依赖关系。

  • 如果一个队列中既包含高优先级操作,又包含低优先级操作,并且两个操作都已经准备就绪,那么队列先执行高优先级操作。比如上例中,如果 op1 和 op4 是不同优先级的操作,那么就会先执行优先级高的操作。

  • 如果,一个队列中既包含了准备就绪状态的操作,又包含了未准备就绪的操作,未准备就绪的操作优先级比准备就绪的操作优先级高。那么,虽然准备就绪的操作优先级低,也会优先执行。优先级不能取代依赖关系。如果要控制操作间的启动顺序,则必须使用依赖关系。

示例:

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        testQueuePriority()
    }
    
    func testQueuePriority(){
        let opA = BlockOperation()
        let opB = BlockOperation()
        let opC = BlockOperation()
        opA.addExecutionBlock {
            print("任务一:\(Thread.current)")
        }
        opB.addExecutionBlock {
            print("任务二:\(Thread.current)")
        }
        opC.addExecutionBlock {
            print("任务三:\(Thread.current)")
        }
        opA.queuePriority = .low
        opB.queuePriority = .high
        opC.queuePriority = .normal
        let queue = OperationQueue()
        queue.maxConcurrentOperationCount = 3
        queue.addOperations([opA, opB, opC], waitUntilFinished: false)
    }
}

Operation、OperationQueue 线程间的通信

在 iOS 开发过程中,我们一般在主线程里边进行 UI 刷新,例如:点击、滚动、拖拽等事件。我们通常把一些耗时的操作放在其他线程,比如说图片下载、文件上传等耗时操作。而当我们有时候在其他线程完成了耗时操作时,需要回到主线程,那么就用到了线程之间的通讯。

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        communication()
    }
    ///线程间通信
    func communication() {
        OperationQueue().addOperation {
            sleep(2)
            print("1---\(Date())\(Thread.current)")
            OperationQueue.main.addOperation({
                sleep(2)
                print("2---\(Date())\(Thread.current)")
            })
        }
    }
}

可以看到:通过线程间的通信,先在其他线程中执行操作,等操作执行完了之后再回到主线程执行主线程的相应操作。

Operation、OperationQueue 线程同步和线程安全

线程同步

  • 线程同步:
    可理解为线程 A 和 线程 B 一块配合,A 执行到一定程度时要依靠线程 B 的某个结果,于是停下来,示意 B 运行;B 依言执行,再将结果给 A;A 再继续操作。

  • 若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作(更改变量),一般都需要考虑线程同步,否则的话就可能影响线程安全。

线程安全

  • 线程安全:
    如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

  • 线程安全解决方案:
    可以给线程加锁,在一个线程执行该操作的时候,不允许其他线程进行操作。iOS 实现线程加锁有很多种方式。@synchronized、 NSLock、RecursiveLock、NSCondition、NSConditionLock、pthread_mutex、dispatch_semaphore、OSSpinLock、atomic(property) set/get 等等各种方式。这里我们使用 NSLock 对象来解决线程同步问题。NSLock 对象可以通过进入锁时调用 lock 方法,解锁时调用 unlock 方法来保证线程安全。

考虑线程安全的代码:

class ViewController: UIViewController {
    var ticketSurplusCount = 50
    override func viewDidLoad() {
        super.viewDidLoad()
        
        ///1.1 创建代表北京火车票售卖窗口
        let operationForBeiJing = OperationQueue()
        operationForBeiJing.maxConcurrentOperationCount = 1;
        ///1.2 创建卖票操作 op1
        let op1 = BlockOperation{ 
            self.saleTicketSafe()
        }
        ///1.3 添加操作
        operationForBeiJing.addOperation(op1)
        
        
        ///2.1创建代表上海火车票售卖窗口
        let operationForShangHai = OperationQueue()
        operationForShangHai.maxConcurrentOperationCount = 1;
        ///2.2创建卖票操作 op2
        let op2 = BlockOperation{
            self.saleTicketSafe()
        }
        ///2.3 添加操作
        operationForShangHai.addOperation(op2)
    }
    
    
    private func saleTicketSafe(){
        while true {
            objc_sync_enter(self)
            if self.ticketSurplusCount > 0 {
                self.ticketSurplusCount-=1;
                print("剩余票数:\(self.ticketSurplusCount) 窗口:\(Thread.current)")
                sleep(2)
            }
            objc_sync_exit(self)
            
            if self.ticketSurplusCount <= 0 {
                print("所有火车票均已售完")
                break
            }
        }
    }
}