いものやま。

雑多な知識の寄せ集め

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

昨日の記事は以下。

今日は、メインキューと実行ループの話。

メインキュー

CocoaやUIKitでUIにさわる場合、メインスレッドでさわるというのがお約束になっている。
なので、標準出力への出力も、本来ならメインスレッドで行うべき。

メインスレッドに関連付けられたオペレーションキューはNSOperationQueue.mainQueue()で得ることが出来る。
このオペレーションキューは、追加された処理をメインスレッドで実行するので、同時に実行できる最大の処理数も最初から1となって、いろいろと都合がいい。

ということで、さっそくメインキューを使うように書き換えてみた。

/* concurrent_test.swift */

import Foundation

let operationQueue = NSOperationQueue()

let operation = NSBlockOperation {}

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

operation.waitUntilFinished()

NSOperationQueue.mainQueue().waitUntilAllOperationsAreFinished()

書き換えの内容は、stdoutAccessQueueNSOperationQueue.mainQueue()に変えただけ。

これを実行すると・・・

$ swift ./concurrent_test.swift
# 何も出力されず、ずっと終わらない。
# なので、Ctrl+Cで強制終了。
^C

というふうに、全然思った通りに動かない。

むぅ、これはなぜか。

割込みの話

ここで、自分が考えていたことを説明するために、ちょっと「割込み」の話を。

プログラムを実行すれば、プログラムは書かれた通りに順番に実行されていくというのが一般的な話。
けど、実際にはそんなことはなかったりする。
というのも、「割込み」があるからだ。

割込みが発生するとプログラムの実行は一時中断され、制御が強制的にシステム側に移ることになる。
そのあと、プログラムが再び実行状態に戻ると、基本的には直前に実行していた状態に戻されるのだけど、実際にはどこにプログラムを戻すのかはシステム次第で、実行する場所を全然別の場所に変えてしまうことも出来てしまう。
例えば、sigaction()というシステムコールを使うと、割込みを抽象化した概念であるシグナルをプロセスが受け取ったときに、実行する場所をシグナルハンドラに変更することが出来たりもする。

じゃあ、この割込みがどのようにして起きるのかというと、それは外部的な要因(キーボードの入力やイーサネットのデータ受信、あるいはタイマーなど)だったり、プログラムによるる内部的な要因(システムコールページフォルト、あるいはゼロ除算など)だったりする。(なお、前者を「外部割込み」、後者を「内部割込み」と呼ぶ)
そして、システムコールというのは実際には内部割込みを起こしているので、システムコールを呼べば現在の実行場所を別の場所に変えることが出来てしまう。

なので、自分が考えていたのは、

  1. メインキューにシステムコールでオペレーションオブジェクトが追加される。
  2. メインスレッドの実行がシステムからの割込みで止められ、実行コンテキストがオペレーションオブジェクトのブロックに変更される。
  3. オペレーションオブジェクトのブロックの実行が終わったら、実行コンテキストが元の場所に返される。

という感じだったのだけど、どうやらそうではないみたい。

実際には、各スレッドがオペレーションキューに入っているオペレーションオブジェクトを取り出してきて、処理をするというループが必要になってくるようだ。

なので、先程のプログラムでは、

  1. メインスレッドでメインキューが空になるのを待っている
  2. そのせいで、メインキューからオペレーションオブジェクトを取り出して実行するという処理をメインスレッドが実行出来ない
  3. したがって、いつまで待ってもメインキューが空にならない

というデッドロックが発生してしまっていた。

実行ループ

さてさて、そこで出てくるのが実行ループ。

実行ループは、オペレーションキューからオペレーションオブジェクトを取り出して実行してくれる他、UIのイベントを処理したり、タイマーの処理をしてくれたりもする。
(UIの処理もタイマーも、割込みが起きて実行コンテキストが飛ばされるのだと思っていたけど、実際には割込みコンテキスト内で処理がキューに追加されるだけで、処理の実行自体は実行ループが行っているっぽい)

メインスレッドの実行ループはNSRunLoop.mainRunLoop()で取得することが出来、NSRunLoop#run()を呼ぶことで実行ループが回り出す。

そこで、プログラムを次のように修正。

/* concurrent_test.swift */

import Foundation

let operationQueue = NSOperationQueue()

let operation = NSBlockOperation {}

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

operation.waitUntilFinished()

NSOperationQueue.mainQueue().addOperationWithBlock {
  exit(0)
}

NSRunLoop.mainRunLoop().run()

一番最後にメインスレッドの実行ループを取得して、実行ループを回すようにしている。
これで、各処理によってメインキューに追加された文字列出力の処理が実行されるようになる。

なお、このままだといつまで経っても実行ループは終わらないので、メインキューの一番最後にプログラムを終了させる処理を追加している。
これでちゃんとプログラムも終了するようになる。

それにしても、一番最後の実行ループの実行が、Tkを使うときのメインループの呼び出しみたいで、なんとも懐かしい感じw

今日はここまで!