いものやま。

雑多な知識の寄せ集め

Swiftでの並列プログラミングについて調べてみた。(その2)

昨日の続き。

隠れた問題

昨日のコードはうまく動いていたように見えるのだけど、実は隠れた問題が。

それを明らかにするために、次のように出力する回数を増やしてみる。

/* concurrent_test.swift */

import Foundation

let operationQueue = NSOperationQueue()
let stdoutAccessQueue = NSOperationQueue()
stdoutAccessQueue.maxConcurrentOperationCount = 1

for i in 1...4 {
  let operation = NSBlockOperation {
    for _ in 1...100 {
      stdoutAccessQueue.addOperationWithBlock {
        println("This operation is \(i).")
      }
    }
  }
  operationQueue.addOperation(operation)
}

さて、これを実行して、出力された回数を調べてみると・・・

$ swift ./concurrent_test.swift | wc
      57     228    1197

なんと、57回しか出力されていない。

4つのオペレーションが100回ずつ出力を行うわけだから、本来なら400回出力されないといけない。
けど、実際に出力されたのは、ずっと少ない回数。
これはなぜなのか?

原因は単純で、キューに入れられた処理を全部終える前にメインスレッドが終わってしまうから。
メインスレッドが終わると、プログラム自体が終わってしまう。
なので、キューに入れられた処理が全部終わるまで、待ってやらないといけない。

キューに入れられた処理が終わるのを待つ

キューに入れられた処理が全部終わるのを待つようにするには、NSOperationQueue#waitUntilAllOperationsAreFinished()を使う。
(あるいは、NSOperationQueue#operationCountを参照してもいい。途中でキャンセルする処理を実装したい場合などは、こちらを使うことになる)

修正したコードが、こちら。

/* concurrent_test.swift */

import Foundation

let operationQueue = NSOperationQueue()
let stdoutAccessQueue = NSOperationQueue()
stdoutAccessQueue.maxConcurrentOperationCount = 1

for i in 1...4 {
  let operation = NSBlockOperation {
    for _ in 1...100 {
      stdoutAccessQueue.addOperationWithBlock {
        println("This operation is \(i).")
      }
    }
  }
  operationQueue.addOperation(operation)
}

stdoutAccessQueue.waitUntilAllOperationsAreFinished()

さて、実行すると・・・

$ swift ./concurrent_test.swift | wc
      95     380    1995

・・・400回出力されていない。

標準出力に出力する処理と標準出力に出力する内容を追加していく処理はそれぞれ並列して動くので、追加よりも先に出力が処理されてしまうと、それでキューが空になってしまうことがある、というのが原因。

なので、

  1. キューに追加する処理が全部終わるのを待つ
  2. そのうえで、キューが空になるのを待つ

としてやらないといけない。

処理が終わるのを待つ

処理が終わるのを待つには、NSOperation#waitUntilFinished()を使う。

/* concurrent_test.swift */

import Foundation

let operationQueue = NSOperationQueue()
let stdoutAccessQueue = NSOperationQueue()
stdoutAccessQueue.maxConcurrentOperationCount = 1

var operations = [NSOperation]()

for i in 1...4 {
  let operation = NSBlockOperation {
    for _ in 1...100 {
      stdoutAccessQueue.addOperationWithBlock {
        println("This operation is \(i).")
      }
    }
  }
  operationQueue.addOperation(operation)
  operations.append(operation)
}

for operation in operations {
  operation.waitUntilFinished()
}

stdoutAccessQueue.waitUntilAllOperationsAreFinished()

出力する内容を追加する各処理が終わるのを待つために、operationsという配列を用意して、そこに各処理のオブジェクトを保持するようにしている。
そして、各処理が終了するのを待ってから、キューが空になるのを待つようにしている。

これを実行すると、次のような感じ。

$ swift ./concurrent_test.swift | wc
     400    1600    8400

これでやっと全部出力されるようになった。

なお、NSBlockOperationには、NSBlockOperation#addExecutionBlock(_: ()->Void)というメソッドも用意されていて、処理のブロックを追加することが出来る。
これらの処理は、オブジェクトがオペレーションキューに追加されると、(可能なら)並列に処理されて、全部終わると「その処理が終わった」と判断される。
なので、次のように書くことも可能。

/* concurrent_test.swift */

import Foundation

let operationQueue = NSOperationQueue()
let stdoutAccessQueue = NSOperationQueue()
stdoutAccessQueue.maxConcurrentOperationCount = 1

let operation = NSBlockOperation {}

for i in 1...4 {
  operation.addExecutionBlock {
    for _ in 1...100 {
      stdoutAccessQueue.addOperationWithBlock {
        println("This operation is \(i).")
      }
    }
  }
}
operationQueue.addOperation(operation)

operation.waitUntilFinished()

stdoutAccessQueue.waitUntilAllOperationsAreFinished()

この場合もちゃんと出力が並列で行われ、きちんと400回出力されるのを待って終了する。

今日はここまで!