いものやま。

雑多な知識の寄せ集め

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

昨日の記事は以下から。

今日は補足的な話。

オペレーションオブジェクトの依存関係

オペレーションオブジェクトには、依存関係を設定することが出来る。

処理Aを行ってから処理Bを行わなければならない場合、処理Bは処理Aに依存していることになる。
その場合、処理Aを処理Bの依存する処理に追加すると、処理Aが終わってからでないと処理Bは実行されなくなる。

オペレーションオブジェクトを依存する処理に追加するには、NSOperation#addDependency(_: NSOperation)を使う。

例えば、次のような感じ。

/* dependency_test.swift */

import Foundation

let opA = NSBlockOperation {
  println("This is first operation.")
}
let opB = NSBlockOperation {
  println("This is second operation.")
}
let exitOp = NSBlockOperation {
  println("bye")
  exit(0)
}

// opBがopAに依存するようにする
opB.addDependency(opA)

// exitOpがopAとopBに依存するようにする
exitOp.addDependency(opA)
exitOp.addDependency(opB)

// メインキューに、
// 依存関係とは逆順に処理を追加してみる。
let mainQueue = NSOperationQueue.mainQueue()
mainQueue.addOperation(exitOp)
mainQueue.addOperation(opB)
mainQueue.addOperation(opA)

NSRunLoop.mainRunLoop().run()
$ swift ./dependency_test.swift
This is first operation.
This is second operation.
bye

キューに追加された順番ではなく、依存関係に従って処理が実行されているのが分かると思う。

なお、依存する処理が全部終わっているかどうかはNSOperation#readyプロパティで知ることが出来る。
依存する処理が全部終わっていて、実行出来る状態になっていれば、このプロパティはtrueを返し、そうでなければfalseを返す。

処理のキャンセル

並列で行ってる処理が長くて、その実行をキャンセルしたい場合がある。
そんなときには、NSOperation#cancle()メソッドを使う。

このメソッドを実行されたオペレーションオブジェクトは、

  • 実行前なら、実行がキャンセルされて、終了済みという扱いになる。
  • 実行中なら、オペレーションオブジェクトがキャンセル状態になる
    (処理が強制的に中断されるわけではないことに注意!)

ここで気をつけないといけないのが、後者。
処理の実行がすでに始まっている場合には、処理の中で「キャンセルされているかどうか?」をこまめにチェックして、キャンセルされていた場合にはその処理を終わらせるというコードが書かれていないと、キャンセルされていようがなんだろうが、処理はずっと続いてしまう
おぉ、なんと前時代的な・・・

例えば、次のようなコードを書いた場合、キーボードから入力があっても処理はキャンセルされない。

/* cancel_test.swift */

import Foundation

let operationQueue = NSOperationQueue()
let operation = NSBlockOperation {
  while true {
    NSOperationQueue.mainQueue().addOperationWithBlock {
      println("Hi!")
    }
    sleep(1)
  }
}
operationQueue.addOperation(operation)

let inputQueue = NSOperationQueue()
inputQueue.addOperationWithBlock {
  let mainQueue = NSOperationQueue.mainQueue()

  NSFileHandle.fileHandleWithStandardInput().availableData
  operation.cancel()
  mainQueue.addOperationWithBlock {
    println("cancel is requested.")
  }

  operation.waitUntilFinished()
  mainQueue.cancelAllOperations()
  mainQueue.addOperationWithBlock {
    println("cancelled!")
    exit(0)
  }
}

NSRunLoop.mainRunLoop().run()
$ swift ./cancel_test.swift 
Hi!
Hi!
Hi!

# Enterキーを押す
cancel is requested.
Hi!
Hi!
Hi!
# ずっと処理が続くので、Ctrl+Cで終了させる
^C

このコードをちゃんと動くようにするには、処理の中でキャンセルされたかどうかをチェックし、キャンセルされていたらその処理を終了させるようにしなければならない。

具体的には、以下のような感じ。

/* cancel_test.swift */

import Foundation

let operationQueue = NSOperationQueue()
let operation = NSBlockOperation {}
operation.addExecutionBlock {
  [unowned operation] in
  while !operation.cancelled {
    NSOperationQueue.mainQueue().addOperationWithBlock {
      println("Hi!")
    }
    sleep(1)
  }
}
operationQueue.addOperation(operation)

let inputQueue = NSOperationQueue()
inputQueue.addOperationWithBlock {
  let mainQueue = NSOperationQueue.mainQueue()

  NSFileHandle.fileHandleWithStandardInput().availableData
  operation.cancel()
  mainQueue.addOperationWithBlock {
    println("cancel is requested.")
  }

  operation.waitUntilFinished()
  mainQueue.cancelAllOperations()
  mainQueue.addOperationWithBlock {
    println("cancelled!")
    exit(0)
  }
}

NSRunLoop.mainRunLoop().run()
$ swift ./cancel_test.swift 
Hi!
Hi!
Hi!

# Enterキーを押す
cancel is requested.
cancelled!

なお、ブロックの中からオペレーションオブジェクトを参照しなければならないので、ちょっと変な書き方になってる。
(※最初はselfで参照できると思ったのだけど、ブロックが書かれている位置はトップレベルであり、NSBlockOperationのコンテキスト内ではないので、selfではオペレーションオブジェクトを参照できない)

それにしても、自前でキャンセルされたかどうかを調べるコードがないと処理がキャンセルされないというのは、なんとも古臭い。
まるで、旧Mac OSでのプログラミングみたいな。

昨日、割込みの話を書いたけれど、Mac OS Xなど普通のOSはプリエンティブマルチタスクという仕組みになっていて、タイマ割込みが入ると強制的に現在のプログラムが一時停止され、制御がシステム側に移るようになっている。
これにより、複数のプログラムを(擬似的に)同時に実行させたり、仮にプログラムが暴走してもシステム側でそのプログラムを強制終了させることが出来るようになっている。
けど、旧Mac OSではこの仕組みになっていなくて、プログラム側が明示的にsleep()を呼び出して制御をシステムに返してやらないと、ずっとそのプログラムが動き続けるようになってた。
なので、プログラムが暴走してしまうとシステム側からはもう手の出しようがなくて、爆弾が出て(懐かしい!)電源長押しで強制再起動させるしかなくなっていた。

オペレーションオブジェクトについても同じで、プログラマ側が明示的に状態をチェックするようにしてやらないと、キャンセルが実現されない。
こういったときにtry-catchによる例外処理の機構があると、システム側で例外を発生させて、処理を例外発生時のブロックに移すことが出来たりする。
なので、Swiftで例外処理が用意されていない弊害は、こんなところにも潜んでいるといえる・・・

もっとも、オブジェクト操作中に意図しないところで例外が発生してしまって、オブジェクトの状態が不正になる危険性もあるので、キャンセルで例外が発生する実装の場合、例外発生を抑止するコードを挟む必要があるというのが難しいところ。
このあたりは設計次第という感じで、例えば割込みが発生することが前提となる組込みの世界では、割込みが起こると困る区間はT-KernelならDI()とEI()で囲むことになっている。
Javaを見てみると、sleep()などでスレッドが寝ている状態なら例外が発生し、そうでない場合は例外は発生せず、処理の方で割込みがされたかどうかをチェックする必要があるようになっている。

今日はここまで!