OperationQueue(NSOperationQueue) で複数の非同期処理の終了を待ち合わせる


はじめに

Swiftで複数の非同期処理の完了時に処理を行うパクリ OperationQueue バージョンといったところです。

まずはコード

func run1() {
    let queue = OperationQueue()
    queue.maxConcurrentOperationCount = OperationQueue.defaultMaxConcurrentOperationCount
    for i in 1...5 {
        queue.addOperation {
            print("#\(i) start. \(Thread.current)")
            sleep(arc4random() % 10 + 1)
            print("#\(i) finish. \(Thread.current)")
        }
    }
    queue.waitUntilAllOperationsAreFinished()
    print("All operations are finished!!")
}

OperationQueue を生成し、Operation を追加していきます。キャンセル不要な単純な処理であれば Operation のサブクラスを作る必要もなく、 addOperation(_ block: @escaping () -> Void) で closure を渡すだけでよいです。ちょっと複雑な状態を持つようになったり、キャンセルできるようにしたくなったら Operation のサブクラスを作成することを検討すればよいと思います。

maxConcurrentOperationCount には、同時に実行する並列数を指定します。OperationQueue.defaultMaxConcurrentOperationCount (デフォルト値)を指定すると、システムはシステム条件に基づいて最大並列数を設定します。1 を指定すれば直列になります。

最後に waitUntilAllOperationsAreFinished() を呼び出すことで、queue に追加されたすべての非同期処理の終了を待ちます。

実行結果

#5 start. <NSThread: 0x60000062e540>{number = 6, name = (null)}
#1 start. <NSThread: 0x60000062e580>{number = 5, name = (null)}
#3 start. <NSThread: 0x60000062ac40>{number = 3, name = (null)}
#2 start. <NSThread: 0x60000062e600>{number = 7, name = (null)}
#4 start. <NSThread: 0x60000062ac80>{number = 4, name = (null)}
#2 finish. <NSThread: 0x60000062e600>{number = 7, name = (null)}
#5 finish. <NSThread: 0x60000062e540>{number = 6, name = (null)}
#4 finish. <NSThread: 0x60000062ac80>{number = 4, name = (null)}
#3 finish. <NSThread: 0x60000062ac40>{number = 3, name = (null)}
#1 finish. <NSThread: 0x60000062e580>{number = 5, name = (null)}
All operations are finished!!

5つのスレッドが生成され、すべての終了後に All operations are finished!! が出力されていることが確認できます。

Operation に依存関係を持たせる

class SleepOperation: Operation {
    let id: String
    let seconds: UInt32
    init(id: String, seconds: UInt32) {
        self.id = id
        self.seconds = seconds
        super.init()
    }

    override func main() {
        print("\(id) start. \(Thread.current)")
        sleep(seconds)
        print("\(id) finish. \(Thread.current)")
    }
}

func run2() {
    let start = Date()
    let queue = OperationQueue()
    let operation1 = SleepOperation(id: "#1", seconds: 1)
    let operation2 = SleepOperation(id: "#2", seconds: 2)
    let operation3 = SleepOperation(id: "#3", seconds: 3)
    let operation4 = SleepOperation(id: "#4", seconds: 4)

    operation3.addDependency(operation1)
    operation3.addDependency(operation2)

    queue.addOperations([operation1, operation2, operation3, operation4], waitUntilFinished: true)
    print("All operations are finished!! (elapsed time: \(String(format: "%.1f", -start.timeIntervalSinceNow)))")
}

Operation のサブクラスを作成した例です。やっていることはほとんど run1 と変わりません。異なるのは、addDependency(_ op: Operation) で operation3 に operation1、operation2 への依存関係を持たせていることです。こうすることで operation3 は operation1 と operation2 の終了を待ってから処理を開始します。

実行結果

#2 start. <NSThread: 0x6000012e4740>{number = 3, name = (null)}
#1 start. <NSThread: 0x6000012e14c0>{number = 5, name = (null)}
#4 start. <NSThread: 0x6000012b70c0>{number = 4, name = (null)}
#1 finish. <NSThread: 0x6000012e14c0>{number = 5, name = (null)}
#2 finish. <NSThread: 0x6000012e4740>{number = 3, name = (null)}
#3 start. <NSThread: 0x6000012e14c0>{number = 5, name = (null)}
#4 finish. <NSThread: 0x6000012b70c0>{number = 4, name = (null)}
#3 finish. <NSThread: 0x6000012e14c0>{number = 5, name = (null)}
All operations are finished!! (elapsed time: 5.0)

operation1 と operation2 が終了してから operation3 が処理を開始していることが確認できます。

まとめ

GCD のラッパーである Operation/OperationQueue で複数の非同期処理の終了を待ち合わせる方法について書きました。キャンセルを受け付けたい場合や、処理に依存関係を持たせたい場合には OperationQueue が便利です。