Ruby: ブロッキングしているスレッドを別スレッドから停止する

2025/02/16に公開

環境

$ 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 あるいは killterminate を使うと Thread を終了できる。
このとき例外処理構文の ensure が呼び出されるので、ensure ブロックに終了処理を書く。

https://docs.ruby-lang.org/ja/latest/method/Thread/i/exit.html

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