いものやま。

雑多な知識の寄せ集め

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

変種オセロをiOSで遊べるようにするために、SpriteKitのサンプルコードを読んでいたのだけど、そこで並列処理が出てきていたので、Swiftでの並列プログラミング(Concurrency Programming)について調べてみた*1

並列プログラミングの手法

Swiftで並列プログラミングを行う場合、次の3通りの方法がある。

  • スレッドを使う
  • ディスパッチキューを使う
  • オペレーションキューを使う

スレッドを使うのは、旧来の方法。
実際のところ、ディスパッチキューを使う方法もオペレーションキューを使う方法も、その足回りではスレッドを使っているので、細かいコントロールを行おうとするなら、スレッドを使わざるをえない場面もあるのかも。
ただ、Appleのドキュメントでは、スレッドを使う方法は設計が難しく、また、下手をするとデッドロックしてしまうことなどもあるので、ディスパッチキューやオペレーションキューを使う方法を推奨している。
なので、今回はノータッチ。

ディスパッチキューは、AppleGrand Central Dispatch(GCD)と呼ぶ機構で、スレッドを管理するコードがシステムレベルで組み込まれているので、プログラマはタスクを定義してそれをキューに追加するだけで、並列処理を実現できるようになっている。
この機能のインタフェースは関数として用意されているので、Swiftはもちろん、C言語でも使用できるというのがいいところ。

そして、オペレーションキューはディスパッチキューをラップしたオブジェクトで、並列処理をよりオブジェクト指向的に扱うことが出来るようになっている。

なので、自分で並列処理を書くのなら、基本的にはオペレーションキューを使えば問題ないと思う。

ただ、ネットを検索すると、ディスパッチキューを使ったコードを書いている人もいる(SpriteKitのサンプルコードもディスパッチキューを使っている)ので、ディスパッチキューを使ったコードも読めるようになっておいた方がいいと思う。

オペレーションキューとオペレーションオブジェクト

オペレーションキューはNSOperationQueueというクラスとして実装されている。
ここにオペレーションオブジェクトを突っ込むと、依存関係や優先度などを考慮して、処理を(可能ならば)並列で実行してくれる。

オペレーションオブジェクトはNSOperation(のサブクラス)のインスタンスで、処理をオブジェクト化したもの。
ただ、NSOperation自体はそのままでは使えないので、あらかじめ用意されているサブクラスか、独自でサブクラスを書いて使う必要がある。
Swiftの場合、基本的にはNSBlockOperationを使えば問題ないと思う。
NSBlockOperationを使うと、ブロック(Swiftだとクロージャ)を使って処理を書き、それをオブジェクトに出来る。

オペレーションオブジェクトの生成

並列処理を行うには、まずはオペレーションオブジェクトを作るところから。

NSBlockOperationを使えば、次のように、ブロックからオペレーションオブジェクトを作ることが出来る。
(※あらかじめFoundationをインポートしておく必要あり)

let operation = NSBlockOperation(block: {
  for _ in 1...5 {
    println("Hello")
  }
})

なお、接尾クロージャを使うことで、次のように書くことも出来る。

// クロージャが一番最後の引数の場合、
// クロージャを実引数列のタプルの外に書くことが出来る。
// さらに、そのとき実引数列が空になるなら、
// メソッド呼び出しの()も省略できる。
let operation = NSBlockOperation {
  for _ in 1...5 {
    println("Hello")
  }
}

すごくRubyっぽいw

なお、こうやって作ったオペレーションオブジェクトは、そのまま実行することも出来る。
そのまま実行するには、NSOperation#start()を呼べばいい。
ただし、その場合、その処理はNSOperation#start()を呼んだスレッドで行われ、その処理が終わるまで制御はスレッドに戻ってこない(同期実行)。

例えば、次のようなコードだと、処理はメインスレッドで順番に実行されるだけで、並列処理にはならない。

/* concurrent_test.swift */

import Foundation

for i in 1...4 {
  let operation = NSBlockOperation {
    for _ in 1...5 {
      println("This operation is \(i).")
    }
  }
  operation.start()
}
$ swift ./concurrent_test.swift
This operation is 1.
This operation is 1.
This operation is 1.
This operation is 1.
This operation is 1.
This operation is 2.
This operation is 2.
This operation is 2.
This operation is 2.
This operation is 2.
This operation is 3.
This operation is 3.
This operation is 3.
This operation is 3.
This operation is 3.
This operation is 4.
This operation is 4.
This operation is 4.
This operation is 4.
This operation is 4.

オペレーションキューの生成

処理を並列で実行したい場合には、オペレーションオブジェクトをオペレーションキューに入れてやる必要がある。
なので、オペレーションキューを作らないといけない。

オペレーションキューを作るには、普通にコンストラクタを呼べばいいだけ。

let operationQueue = NSOperationQueue()

オペレーションオブジェクトの追加

オペレーションキューを作ったら、そこにオペレーションオブジェクトを追加する。
オペレーションキューにオペレーションオブジェクトを追加するには、NSOperationQueue#addOperation(_: NSOperation)を使えばいい。
そうすると、オペレーションキューからオペレーションオブジェクトが取り出され、処理が並列で行われる。

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

/* concurrent_test.swift */

import Foundation

let operationQueue = NSOperationQueue()

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

さて、これを実行すると・・・

$ swift ./concurrent_test.swift
TTTThhhhiiiissss    ooooppppeeeerrrraaaattttiiiioooonnnn    iiiissss    1234....



TTTThhhhiiiissss    ooooppppeeeerrrraaaattttiiiioooonnnn    iiiissss    1234....



TTTThhhhiiiissss    ooooppppeeeerrrraaaattttiiiioooonnnn    iiiissss    1234....



TTTThhhhiiiissss    ooooppppeeeerrrraaaattttiiiioooonnnn    iiiissss    1234....



TTTThhhhiiiissss    ooooppppeeeerrrraaaattttiiiioooonnnn    iiiissss    1234....


!?

処理は確かに並列で行われているっぽいのだけど、あきらかに出力がおかしい。
なぜ・・・

リソースアクセスの排他制御

原因は、排他制御しなければならないリソースへ並列してアクセスしようとしたから。

今回の場合、標準出力というリソースに対して、各オペレーションオブジェクトが一斉にアクセスを行ったので、このように各オペレーションオブジェクトからの出力が入り乱れる結果となってしまっている。

これを防ぐには、排他制御ーーすなわち、あるオペレーションオブジェクトが"This operation is \(i)."という文字列を標準出力に出力しきるまでは、他のオペレーションオブジェクトが標準出力に触れないようにする必要がある。

排他制御を実現する一つの方法としては、セマフォを使う方法があるけど、オペレーションキューを使えばセマフォを使わなくても排他制御を実現できる。

どのようにすればいいのかというと、クリティカルセクション(リソースに他の処理が触らないようにする必要がある区間)の処理をオペレーションオブジェクトにし、それをリソースへアクセスする処理専用のオペレーションキューに登録する。
そして、そのオペレーションキューが同時に実行できる処理の最大数を1にしておけば、そのリソースへアクセス出来るのは、現在実行されているクリティカルセクションのオペレーションオブジェクトのみになる。

コードで書くと、次のような感じ。

// リソースへアクセスする処理専用のオペレーションキューを作り、
// 同時に実行できる処理の最大数を1にしておく。
let resourceAccessQueue = NSOperationQueue()
resourceAccessQueue.maxConcurrentOperationCount = 1

let criticalSection = NSBlockOperation {
  // 排他制御の必要なリソースへの
  // アクセスを行う処理
}

resourceAccessQueue.addOperation(criticalSection)

先ほどのコードをこのように書き直してみたのが、以下。

/* 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...5 {
      stdoutAccessQueue.addOperationWithBlock {
        println("This operation is \(i).")
      }
    }
  }
  operationQueue.addOperation(operation)
}

なお、途中でNSOperationQueue.addOperationWithBlock(_: ()->Void)を使っていて、これを使うとブロックを直接オペレーションキューに追加することが出来る。

これを実行させた結果が、以下。

$ swift ./concurrent_test.swift
This operation is 1.
This operation is 3.
This operation is 2.
This operation is 1.
This operation is 4.
This operation is 3.
This operation is 2.
This operation is 1.
This operation is 4.
This operation is 3.
This operation is 2.
This operation is 1.
This operation is 4.
This operation is 3.
This operation is 2.
This operation is 1.
This operation is 4.
This operation is 3.
This operation is 2.
This operation is 4.

見ての通り、"This operation is \(i)."の文字列がバラバラにならずに出力されている。
そして、出力の順番は各処理が並列して動いているので、バラバラになっている。

なお、一つ気をつけないといけないのが、オペレーションキューが同時に実行できる処理の最大数を1にしても、(キューという名前に反して)一番最初にキューに入れられた処理が一番最初に実行されるとは限らないということ。
どの順番で処理されていくのかは、オペレーションオブジェクト間の依存関係であったり、優先度などで決まってくる。
(GCDの直列ディスパッチキューの場合、FIFOで処理されていく)

「UIにさわる処理は専用のスレッドのみで」の正体

GUIプログラミングをしていると当たり前のように出てくるお約束が、「UIにさわる処理は専用のスレッドのみで」というもの。
CocoaやUIKitであれば、UIにさわる処理はメインスレッドで行わなければならない約束だし、JavaのSwingを使う場合も、イベントディスパッチスレッドで行わなければならないということになっている。
SwingのSwingUtilitiesやSwingWorkerに苦しめられた人も多いはず・・・

その理由は、つまりこういうこと。
UIというリソースへのアクセスが勝手気ままに行われてしまうと、今回の失敗例のような変な状態になってしまう可能性がある。
なので、排他制御が必要なのだけど、セマフォミューテックスを使って排他制御を行うのだと、処理が重たくなったり、最悪デッドロックに陥ってしまう可能性もある。
そこで、UIにアクセスするスレッドを1つに限定し、キューに追加されたUIのイベントやUIにさわる処理を1つずつ処理していくことで、排他制御を実現しているというわけ。

さて、これで何も問題はなくなったのかというと、実はまだ問題が残っていて・・・
それについてはまた明日。

今日はここまで!

*1:Wikipediaを見ると、Concurrencyは並行、Parallelは並列と訳されている。これらは別の概念なので使い分けないといけないのだけど、ここではAppleの日本語ドキュメントの訳し方にしたがい、「並列プログラミング」とした