いものやま。

雑多な知識の寄せ集め

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

もうちょっとだけ続くんじゃ。

ディスパッチキュー

並列処理を書くもう一つの方法が、ディスパッチキューを使う方法。

といっても、内容はオペレーションキューを使うのとほぼ一緒。
基本的にはディスパッチキューにブロック(Swiftの場合、クロージャ)を追加すれば、その処理を(必要かつ可能なら)並列に処理してくれる。

ただ、ちょっと違うのは、

  • インタフェースがオブジェクトのメソッド呼び出しではなく、関数呼び出しである。
  • 処理間の依存関係は管理せず、追加された順に処理を開始する。
  • 直列ディスパッチキューと並列ディスパッチキューという2種類がある。

ということ。

直列ディスパッチキューと並列ディスパッチキュー

ディスパッチキューには、直列ディスパッチキューと並列ディスパッチキューがある。

種類 説明
直列ディスパッチキュー 処理を追加された順に実行する。
キューに追加された処理で同時に実行されるものは、最大1つ。
並列ディスパッチキュー 処理を追加された順に実行する。
キューに追加された処理は、可能なら複数のものが並列して行われる。

なお、複数の直列ディスパッチキューがあった場合、それぞれに追加された処理は並列して動く場合もある。
(同時に動くことがないことが保証されているのは、同じ直列キューに追加された処理のみ、ということ)

このことから、排他制御が必要なリソースへのアクセスを行う処理は直列ディスパッチキューへ追加し、並列して行いたい処理は並列ディスパッチキューへ追加するといい。

なお、メインスレッドに関連付けられたディスパッチキューとして、メインディスパッチキューがある。
メインディスパッチキューは直列ディスパッチキューになっている。

ディスパッチキューの生成と取得

ディスパッチキューを生成するには、dispatch_queue_create()を使う。

// 直列ディスパッチキューを生成
let serialQueue = dispatch_queue_create("com.hoge.app.serial", DISPATCH_QUEUE_SERIAL)
// 並列ディスパッチキューを生成
let concurrentQueue = dispatch_queue_create("com.hoge.app.concurrent", DISPATCH_QUEUE_CONCURRENT)

第1引数に指定しているのはディスパッチキューのラベルで、逆DNSのスタイルが推奨されている。

なお、並列ディスパッチキューはアプリのどこからでも使えるもの(グローバルな並列ディスパッチキュー)があらかじめ4つ用意されているので、それを取得して使うこともできる。
グローバルな並列ディスパッチキューを取得するには、dispatch_get_global_queue()を使う。

// 第1引数には、次のいずれかの優先度を指定する。
// - DISPATCH_QUEUE_PRIORITY_HIGH
// - DISPATCH_QUEUE_PRIORITY_DEFAULT
// - DISPATCH_QUEUE_PRIORITY_LOW
// - DISPATCH_QUEUE_PRIORITY_BACKGROUND
// 第2引数は将来の拡張用で、今は常に0を指定する。
let defaultGlobalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)

また、メインディスパッチキューはdispatch_get_main_queue()で取得できる。

// メインディスパッチキューの取得
let mainQueue = dispatch_get_main_queue()

処理の追加

ディスパッチキューに処理を追加するには、dispatch_async()を使う。

dispatch_async(処理を追加するディスパッチキュー, 追加する処理のブロック)

// Swiftなら接尾クロージャを使うことも出来る
dispatch_async(処理を追加するディスパッチキュー) {
  追加する処理の内容
}

こうやって追加すると、ディスパッチキューが処理をよろしく実行してくれる。

なお、dispatch_async()を使うと、処理をディスパッチキューに追加したら制御はすぐに呼び出し元に戻ってくるけれど、ディスパッチキューに処理を追加して、その処理が終わるまで呼び出し元に制御を戻したくない場合には、dispatch_sync()を使うといい。(呼び出し方はdispatch_async()と同じ)

処理の終了を待つ

処理の終了を待ちたい場合には、ディスパッチグループを使う。

ディスパッチグループを生成するには、次のようにする。

let group = dispatch_group_create()

そして、処理を追加するときに、dispatch_async()の代わりにdispatch_group_async()を使う。

dispatch_group_async(group, 処理を追加するディスパッチキュー, 追加する処理のブロック)

あとは、dispatch_group_wait()を使うことで、同じディスパッチグループとしてディスパッチキューに追加された処理が全部終わるまで、待つことが出来る。

dispatch_group_wait(group, DISPATCH_TIME_FOREVER)

なお、第2引数でDISPATCH_TIME_FOREVERを指定するとずっと待ってくれるし、時間を指定するとタイムアウトするとのこと。
(※ただ、実際にはDISPATCH_TIME_FOREVER以外だと上手く動かなかった)

その他

繰り返し処理の並列化

for文で繰り返し処理を行う場合、それぞれの繰り返しが独立していて、かつ、1回の処理が重い場合、それぞれの処理を並列して行うといい。

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

let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)
for i in 0..<count {
  dispatch_async(queue) {
    // 繰り返す処理
  }
}

これを簡単に書くために、dispatch_apply()という関数が用意されている。

let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)
dispatch_apply(count, queue) { i in
  // 繰り返す処理
}

そんなに変わらないじゃんとか、たぶん言っちゃいけないw
まぁ、自分で使うことはないと思うけど、使われてるコードで困るといけないので、頭の片隅に置いておいた方がいいかもしれない。

セマフォ

排他制御は、普通は直列ディスパッチキューを使えば問題ないのだけど、一応セマフォも用意されている。
排他制御が必要な複数リソースへのアクセスを行う場合に、そのリソースの獲得順番が決まっている場合には、セマフォを使った方が分かりやすいのかも。
(※排他制御が必要なリソースが複数ある場合、そのアクセス順を決めて守らないと、デッドロックに陥る危険性がある)

セマフォを生成するには、dispatch_semaphore_create()を使う。

// 第1引数で、利用可能なリソース数の初期値を指定する
let semaphore = dispatch_semaphore_create(1)

セマフォを獲得するには、dispatch_semaphore_wait()を使う。

// セマフォを獲得しようとする。
// 利用可能なリソースがない場合、セマフォは獲得できず、
// 利用可能なリソースが増えるまで待ち状態になる。
// 利用可能なリソースがある場合、セマフォを獲得でき、
// 利用可能なリソース数が1減る。
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)

セマフォを返すには、dispatch_semaphore_signal()を使う。

// セマフォを返す。
// 利用可能なリソース数を1増やす。
dispatch_semaphore_signal(semaphore)

なお、セマフォには排他制御以外にも使い方があるけど、基本的にはディスパッチキューを使えば問題ないはず。

今日はここまで!