[Ruby] スレッド数固定で処理を行う

お仕事で、数百台のサーバに対して、多少時間のかかる処理をする必要があった。

1 つずつ実行していては、さすがに日が暮れてしまう。
そうなるとスレッドで並列実行したいが、対象のサーバの分、数百個スレッドを生成すると逆に重くなってしまう。

というわけで、スレッド数に上限を設けて実行した、という話。

雛形はこんな感じ。

#!/usr/bin/ruby -Ku
# -*- coding: utf-8 -*-
#
# 20120619
# thread template
#

require 'thread'


def job(arg)
  sleep rand(0)
  print "#{arg}\n"
end


def main
  # スレッド数上限
  thread_max = 3

  # キュー
  jobqueue = Queue.new
  (1..10).each do |n|
    jobqueue.push(n)
  end

  # スレッドで処理
  threads = []
  thread_max.times do
    threads << Thread.start do
      while !jobqueue.empty?      # (*1)
        var = jobqueue.pop        # (*2)
        job(var)
      end
    end
  end

  # スレッド完了待ち
  threads.each {|t| t.join}
end

### main ###
main

jobqueue にジョブを追加して、thread_max 数のスレッドで jobqueue から取り出しながら処理する。
複数のレジがあって、客の行列が 1 つあるイメージでしょうか。

実際は、キューに対象の IP を push して、thread_max = 30 にする感じで使いました。

while !jobqueue.empty? してから pop しているのがポイントといえばポイント。
while var = jobqueue.pop とすると、pop でキューが空のときスレッドが sleep してしまい、以下のように、deadlock してしまう。

% ./test_bad.rb
1
2
5
6
4
8
7
3
9
10
deadlock 0x7fcca4541b90: sleep:-  - ./test_bad.rb:31
deadlock 0x7fcca455f370: sleep:J(0x7fcca4541b90) (main) - ./test_bad.rb:38
deadlock 0x7fcca45418c0: sleep:-  - ./test_bad.rb:31
deadlock 0x7fcca4541a28: sleep:-  - ./test_bad.rb:31
./test_bad.rb:31: [BUG] Segmentation fault
ruby 1.8.7 (2011-06-30 patchlevel 352) [x86_64-linux]

zsh: abort      ./test_bad.rb
  • 追記 (2012/06/25)

このコードだとまだ問題がある事に後から気づいた。
*1 でキューが空かどうか確認してから、*2 での pop までの間に、他のスレッドが pop をすると、やはり deadlock してしまう。
Mutex を使うなどして排他制御するといいだろう。