⛔
Ruby: ブロッキングしているスレッドを別スレッドから停止する
環境
$ ruby --version
ruby 3.2.3 (2024-01-18 revision 52bb2ac0a6) [x86_64-linux]
課題
以下のように TCPServer の accept によってスレッドがブロッキングしているときに、ブロッキングを中断し、終了処理をしたい。
以下のコードは、TCPServer のスレッドを停止させる前にメインスレッドとプロセスを終了させてしまうため、bye
が表示されない。
require 'socket'
thread = Thread.new do
TCPServer.open("localhost", 8999) do |server|
loop do
socket = server.accept
socket.close
end
end
puts 'bye' # 終了処理
end
sleep 1
# TODO: thread を止める
exit
解決策
Ruby ではスレッドに対していくつかシグナルを送ることができる。便利。
begin-ensure を使う
Thread#exit
あるいは kill
、 terminate
を使うと Thread を終了できる。
このとき例外処理構文の ensure
が呼び出されるので、ensure
ブロックに終了処理を書く。
require 'socket'
thread = Thread.new do
begin
TCPServer.open("localhost", 8999) do |server|
loop do
socket = server.accept
socket.close
end
end
ensure
puts 'bye' # 終了処理
end
end
sleep 1
thread.exit
thread.join
exit
Thread#exit
を明示的に呼び出さなくても、メインスレッドが終了する時など、そのスレッドが終了するタイミングであれば ensure
は呼び出される。
require 'socket'
thread = Thread.new do
begin
TCPServer.open("localhost", 8999) do |server|
loop do
socket = server.accept
socket.close
end
end
ensure
puts 'bye' # スレッドが終了した時も呼び出される
end
end
sleep 1
# thread.exit # thread.exit しなくても
# thread.join
exit # メインスレッドが終了するタイミングで子スレッドも終了する
例外を投げる
Thread.raise
で例外を投げることができるので、スレッド側で例外を rescue
捕捉することでブロッキングを中断できる。
require 'socket'
class MyInterrupt < Exception
end
thread = Thread.new do
begin
TCPServer.open("localhost", 8999) do |server|
loop do
socket = server.accept
socket.close
end
end
rescue MyInterrupt
puts 'bye1'
ensure
puts 'bye2'
end
end
sleep 1
thread.raise MyInterrupt
thread.join
exit
例外を rescue
で捕捉さえすればスレッド自体は継続できるのが、ensure
方式との差別点。
ただし、例外はブロッキング中以外のタイミングでも発生する可能性はあるので、コードの流れを完全に把握出来ないのであれば利用は避けたほうが良い。
require 'socket'
class MyInterrupt < Exception
end
thread = Thread.new do
TCPServer.open("localhost", 8999) do |server|
loop do
socket = server.accept
socket.close
rescue MyInterrupt
puts 'Interrupt'
end
end
ensure
puts 'bye1'
end
sleep 1
# accept のブロッキングを中断、再試行する
thread.raise MyInterrupt
sleep 1
thread.exit
thread.join
exit
Discussion