組み込みクラス: Thread

Threadの作成は例にあるnewの他startforkが使える。

t = Thread.new{sleep 0.5}
p t.status # "run"
sleep 1
p t.status # "false" 正常終了済みの意味
p t.alive? # "false"

Thread.stopでスレッドを停止させることが出来る。 これにより、ステータスはsleepとなる。 スレッド生成直後はrunになってしまうが、直ちにsleepに切り替わっているのがわかる。

t = Thread.new{ Thread.stop }
p t.status # "run"
sleep 1
p t.status # "sleep"
p t.alive? # "false"

最下位にはwakeuprunを用いる。

t = Thread.new{ 
  Thread.stop 
  puts "Bye!"
}
p t.status # "run"
sleep 1
p t.status # "sleep"
t.wakeup
sleep 1
p t.status # Bye! false

スレッドの終了はkillexitを用いる。またクラスメソッドのThread.killThread.exitも終了に使えるが、前者は引数に殺すメソッドを指定する、後者はカレントスレッドを殺すという違いがある。

t = Thread.new{ 
  Thread.stop 
  puts "Bye!"
}
p t.status # "run"
sleep 1
p t.status # "sleep"
Thread.kill(t)
sleep 1
p t.status # "false" killしても異常終了にはならない。

exitはかなり挙動が異なる。

t = Thread.new{ 
  Thread.stop 
  puts "Bye!"
}
p t.status # "run"
sleep 1
p t.status # "sleep"
t.instance_eval("Thread.exit") # main終了
# TIPS: instance_evalは文字列を渡しても外側のコンテキストで実行されてしまう。
sleep 1    # 実行されない。
p t.status # 実行されない。

スレッドにはensure節が設置でき、終了直前やkillされたりしたときに実行される。

t = Thread.new{ |t|
  begin 
    Thread.stop 
    puts "Bye!"
  ensure
    puts "killed. Bye!"
  end
}
p t.status # "run"
sleep 1
p t.status # "sleep"
t.kill
sleep 1 # "killed. Bye!"
p t.status # "false"

スレッドで例外が発生する場合がある。その場合は警告なく異常終了する。 但しrescue節を設置すると、例外が発生しても異常終了させず"false"のステータス(正常終了)とすることが出来る。

t = Thread.new{ |t| 
    Thread.stop 
    raise
}
p t.status # "run"
sleep 1
t.wakeup
sleep 1
p t.status # "nil" 異常終了

rescue節を設置してみると...

t = Thread.new{ |t| 
    begin 
      Thread.stop 
      raise "Error!"
    rescue => e
      p e.message
    end
}
p t.status # "run"
sleep 1
t.wakeup "Error"
sleep 1
p t.status # "false"

スレッドはその動作を他からのスレッドの処理を待って再開させる、joinという機能がある。joinを待っているスレッドで例外が発生した場合、その例外は待機中のスレッドにも同様の例外が発生する。

t_c = Thread.new{ |t|  
      raise "Error in child!"
}

t = Thread.new{ |t| 
    begin 
      Thread.stop 
      t_c.join
    rescue => e
      p e.message
    end
}
p t.status # "run"
sleep 1
t.wakeup   # "Error in child!"
sleep 1
p t.status # "false"

スレッドのデッドロックとは 存在する複数のスレッドすべてが停止状態となっている状態をいう。

t = Thread.new{ |t| 
      Thread.stop  # tスレッドをストップさせる。
}
Thread.stop # mainスレッドのストップ

これにより以下のようなエラーが表示される。

thread.rb:22:in `stop': No live threads left. Deadlock? (fatal)
        from thread.rb:22:in `<main>'

killやexitではだめであくまで、stopさせた状態でないとプログラムは正常終了してしまう。

スレッドクラスのクラスメソッドはスレッド一覧を表示する機能や、メインスレッド、カレントスレッドを表示する機能を提供している。

puts Thread.list # #<Thread:0x007fffc5a52608>

t = Thread.new{ |t| 
      Thread.stop 
      puts Thread.current # #<Thread:0x007fffc5a52608>
}

puts Thread.list # #<Thread:0x007fffc5a52608>
puts Thread.main # #<Thread:0x007fffc5e532e8>
t.wakeup

明示的に指示しなければ、mainスレッドは表示されないようだ。

他のスレッドの終了を待つjoinメソッドは、更にスレッドの評価結果も返してくれるvalueが上位互換メソッド的に存在する。

t_c = Thread.new{ |t|  
      sleep 5
      "Yeah!"
}

t = Thread.new{ |t| 
  Thread.stop 
  puts t_c.value "Yeah!"
  puts "finish!"
}
sleep 7
t.wakeup
sleep 1
p t.status # "false"

スレッドはハッシュと同じような機能を持ち、キーと値をペアを簡単に引き出せる。

t = Thread.current
t[:place]  = "ochanomizu"
p t[:place] # "ochanomizu"
p t.keys # [:__recursive_key__, :place]

Mutexは組み込みクラスとしては実装されておらず、標準添付ライブラリとしての実装でreuqireが必要である。 Mutexとは排他制御を行う仕組みのことで、通常のThread.newの内側でThread.exclusiveというクラスメソッドの実行によってできたスレッドを使用する。 このスレッドが実行されている間、他のスレッドは一切実行されることがなく、そのスレッドの動きだけに拘束される。

s_ex = Thread.new{
  Thread.exclusive{
    5.times {
      print "S"
      sleep 1
    }
  }
}

t_ex = Thread.new{
  Thread.exclusive{
    5.times {
      print "T"
      sleep 1
    }
  }
}

s_ex.join
t_ex.join
# "SSSSSTTTTT" と表示される。
puts "\n"

これをexclusiveさせないと、

s_ex = Thread.new{
    5.times {
      print "S"
      sleep 1
    }
}

t_ex = Thread.new{
    5.times {
      print "T"
      sleep 1
    }
}

s_ex.join
t_ex.join
# "STSTSTSTST" と表示される。
puts "\n"

これを更に柔軟に、一スレッド全体ではなく特定の処理を行うときに他のスレッドに侵入させない様ロックを行うのが`Mutex.new.lockである。

Mutexインスタンスは制御を行いたい複数のスレッドに対して、1つのインスタンスで管理させる必要があるようだ。

@m = Mutex.new
s_ex = Thread.new{
    @m.lock
    5.times {
      print "S"
      sleep 1
    }
    @m.unlock
}

t_ex = Thread.new{
  @m.lock
    5.times {
      print "T"
      sleep 1
    }
  @m.unlock
}

s_ex.join
t_ex.join
# "SSSSSTTTTT" と表示される。
puts "\n"

既にlockが行われているmutexはunlock状態が起こるまで待つことが出来る。また、ロック状態を確認するためにlocked? try_lockというメソッドが用意されている。

@m = Mutex.new
s_ex = Thread.new{
    @m.lock
    5.times {
      print "S"
      sleep 1
    }
    @m.unlock
}

t_ex = Thread.new{
  puts @m.locked? # "True"
  @m.lock
    5.times {
      print "T"
      sleep 1
    }
  @m.unlock
}

s_ex.join
t_ex.join
# "StrueSSSSTTTTT" と表示される。
puts "\n"

以上の例から、2つ目のスレッドが一度排他制御を行おうとするものの、1つ目のスレッドがロックをかけているため待機しているのがわかる。

最後にFiberについて少し触れる。 FiberはThreadと異なり、明示的に指定(resume)しないと始まらないし、処理が終了しても勝手に親スレッドに戻ったり(yield)しない。明示的な操作が常に必要になるのが特徴である。

f = Fiber.new do
  loop do
    puts "I am Child!"
    puts ">> Child -> Parent"
    Fiber.yield
  end
end

3.times do
  puts "I am Parant"
  puts ">> Parent -> Child"
  f.resume
end

# I am Parant
# >> Parent -> Child
# I am Child!
# >> Child -> Parent
# I am Parant
# >> Parent -> Child
# I am Child!
# >> Child -> Parent
# I am Parant
# >> Parent -> Child
# I am Child!
# >> Child -> Parent