OperationとOperationQueue
複数の非同期な処理を順に実行する方法を検討していて、OperationQueueを使うと便利そうだなと思い触ってみました。
OperationとOperationQueue
キュー(OperationQueue)の中に処理(Operation)を追加しておくと、先に追加した処理から順に実行され、実行中の処理が終了すると次の処理実行される仕組みです。終了した処理はQueueから取り除かれます。
並列で処理する数を制限したり、「処理Aが終わってから処理Bを実行したい」というような依存関係も設定できます。
非同期で実行する場合
まずはOperationを継承したクラスを用意し、実行したい処理はstart()メソッドをoverrideして中に記述します。また、isConcurrent
をtrue
に設定することで並列に実行することが可能になります。
class Operation: Foundation.Operation { override var isConcurrent: Bool { return true } override func start() { // 処理 } }
非同期処理の終了を検知する
ある処理が終わった後に次の処理を実行するためには、処理が終了したことを検知する必要があります。Operationでは、isFinished
プロパティを監視(KVO)して検知する仕組みです。例えば、適当にState
で状態を管理しつつ、state = .finished
になったらisFinished
の値が変更されたことを通知するようにしています。isFinished
プロパティがread-onlyなため、willChangeValue(forKey:)
とdidChangeValue(forKey:)
を使って通知するしかなさそうです。
class Operation: Foundation.Operation { enum State { case ready, started, finished } private var state: State = .ready { willSet { willChangeValue(forKey: #keyPath(Operation.isFinished)) } didSet { didChangeValue(forKey: #keyPath(Operation.isFinished)) } } override var isFinished: Bool { return state == .finished } }
KVOでキーを指定する場合は、#keyPath
を使うと便利です。プロパティ名を文字列で指定してしまうと、コンパイルエラーにならなかったりするので面倒です。
キャンセル処理
OperationQueueにはcancelAllOperations()
メソッドがあり、キュー内の全てのOperationのcancel()
メソッドを呼ぶことが出来ます。ただし、isCanceled
プロパティが変更されるだけで、実際にOperationが実行されなくなるわけでも、キューから取り除かれるわけでもありません。キャンセルの処理は自身で記述する必要があります。
また、まだ実行されていないOperationは、キャンセルされた後であればQueueから即座に取り除くことが可能だそうです。
class Operation: Foundation.Operation { override func cancel() { super.cancel() state = .finished } }
Queueに詰める
OperationQueueに詰めると即座に実行されていきます。
class ViewController: UIViewController { private lazy var queue: OperationQueue = { let queue = OperationQueue() queue.maxConcurrentOperationCount = 1 // 同時実行処理を1つに制限 return queue }() override func viewDidLoad() { super.viewDidLoad() (0...10).forEach { (num) in let operation = Operation() let name = "\(num)" operation.name = name operation.completionBlock = { print("operation finished:", name) } queue.addOperation(operation) } } }
おまけ:KVO
#keyPath
はプロパティ変更の通知を受け取った先でも使えます。便利!
override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { guard let keyPath = keyPath, let change = change else { return } if keyPath == #keyPath(Operation.isFinished) { guard let old = change[.oldKey] as? Bool, let new = change[.newKey] as? Bool else { return } if !old && new { print("operation is finished.") } } }