対象:
CRuby
JRuby

JRubyのスレッドと同期

JRubyだとスレッドはまじめに並列に動作するようだ。

  1. スレッド
  2. スレッドローカル変数の利用
  3. ミューテックス
  4. スレッドの終了を待機する

スレッド

Rubyでスレッドの作成を行うにはThread.newメソッドを利用する。その際、newメソッドには引数をいくつでも渡せるので、スレッドの生起時に値が変わる可能性のある変数はここで渡しておく必要がある。が、とりあえずそのようなことは考えずに書いてみよう。

for i in 0..9
  Thread.new {
    puts "Hello World #{i}" # このコードはまずい
  }
end

上記のコードの実行結果は以下である。(NetBeans 6.8で実行。JRubyは1.4.0)

Hello World 3
Hello World 3
Hello World 2
Hello World 6
Hello World 2Hello World 4
Hello World 6

Hello World 8
Hello World 9
Hello World 9

想定内だがおかしな結果である。ご覧いただければお分かりの通り、0~9の数字がそろっていないばかりか、Hello Worldがくっついてしまっていたり、改行だけの行があったりと問題がある。

スレッドローカル変数の利用

まず既述の通り、iはスレッドが生起した時点では値が変わっている可能性がある。何故なら、親スレッドではforループが進み、子スレッドが生起した時点ではiが増加しているからである。実際、変わっているので0~9の数字はそろわない。

これを解消するためにはiをThread.newメソッドの引数として渡してやる。すると、子スレッドのブロックにはスレッドローカルな変数として渡されるので、子スレッドが生起した時点でもThread.newメソッドをコールした時点の値が保持されている。

for i in 0..9
  Thread.new(i) {|thread_local_i|
    puts "Hello World #{thread_local_i}" # 0-9が表示される
  }
end

修正したコードの実行結果は以下である。このとき重要なのは、新たに生成するスレッド内の変数は元(親スレッド)の変数とは名前を変えることである。上記の変数iは子スレッドのブロックにはthread_local_iという別な変数として渡される。同じ内容でスレッドを複数生成したとしても、それぞれ別世界のthread_local_iができる(つまり、スレッド固有なコピー)。

もし同じ変数名にするとJRuby 1.4.0では親スレッドの変数iが優先されるようである。もちろん、親スレッドの変数を意図する場合は、それは問題なくできるが排他を考える必要があるかも知れない。

Hello World 4
Hello World 6
Hello World 3
Hello World 1Hello World 5

Hello World 2
Hello World 0
Hello World 7
Hello World 8
Hello World 9

0~9の役者はそろったが、まだ"Hello World 1"と"Hello World 5"が結合しており、更にその下は改行のみとなっている。これを解消するにはミューテックスによる同期が必要となる。

ミューテックス

上記のHello Worldが結合してしまう現象は、Hello World 1を表示して改行をする前にHello World 5のスレッドが割り込んできてputsしてしまったからである。つまり、puts "Hello World #{thread_local_i}"はアトミックではないのである。これを防ぐにはミューテックスで同期する必要がある。

ミューテックスはRubyが持つ排他制御機構の1つで、Mutex.newメソッドで簡単に作成することができる。更にsynchronizeメソッドを使用すれば、いちいちミューテックスのロックやアンロックを書く必要もないので手軽に利用できる。

# これが問題が解消されたコード
require "java"
require "thread"

m = Mutex.new # ミューテックスの生成
for i in 0..9
  Thread.new(i) {|thread_local_i|
    m.synchronize {
      puts "Hello World #{thread_local_i}" # ブロック内は他スレッドから割り込み不可
    }
  }
end

これですべての問題が解消された。

Hello World 0
Hello World 3
Hello World 1
Hello World 5
Hello World 2
Hello World 4
Hello World 6
Hello World 7
Hello World 8
Hello World 9

スレッドの終了を待機する

複数のスレッドで平行して処理を行い、それらすべての処理が終わってから次の処理に進みたい場合がある。そのような場合はThread.joinで各スレッドの終了を待機する。Thread.newメソッドが返すThreadオブジェクトを保持しておき、それぞれでjoinする。

joinメソッドをコールしたスレッドは(下記のコードの場合はメインスレッド)停止し、待機対象のスレッドが終了するとメソッドから戻る。

threads = []
p Time.now
threads << Thread.new {sleep(2)}
threads << Thread.new {sleep(3)}
threads << Thread.new {sleep(4)}

threads.each {|t|
  p t.join
  p Time.now
}

実行結果は以下のようになる。開始から2秒後、3秒後にそれぞれ1番目、2番目のスレッドが終了し、最後のスレッドが終了する4秒後にメインスレッドの処理も終了している。

Thu Mar 04 19:06:30 +0900 2010
#<Thread:0x1fc1a6dead>
Thu Mar 04 19:06:32 +0900 2010
#<Thread:0xe29820dead>
Thu Mar 04 19:06:33 +0900 2010
#<Thread:0x718242dead>
Thu Mar 04 19:06:34 +0900 2010
class Thread
class Mutex
(2010/03/05)

新着情報
【オープンソースソフトウェア環境構築】Apple silicon Macで開発環境を構築
【Rust Tips】Actix webでJSONをPOSTする
【Rust Tips】コマンドライン引数を取得する

Copyright(C) 2004-2014 モバイル開発系(K) All rights reserved.
[Home]