Threadの話

最近Rubyでクローラを書いた。
なかなか気合いの入った動きをみせ、一晩で3Gバイトものデータをダウンロードしてくる。
また、それに比例して処理も遅くなる為、Threadを使うことにした。

RubyでのThreadはグリーンスレッド、つまり1つのカーネルスレッドに対して複数のユーザースレッドが動作している為、並行的には動作できるが、並列には動かない。つまり速くならない。
しかしクローラのようにボトルネックがネットワークとディスクIOの場合、待ち時間に他の処理ができるのでそこそこ有用です。


問題としてRubyのThreadはとにかく遅い。コストが高すぎ。スイッチング遅すぎ。
本来は

(1..10000).map { |e| Thread.start { e**2 } }.map{ |th| th.value }

みたいにThreadを使い捨てにしたい。楽だし。
でもリソースがもったいなさ過ぎな上に逆に遅くなる。Matzも「Threadをいじめないで」と言うくらいに。

エコな時代はソフトウェアにも優しくしないといけない為、簡単なThreadPoolを書いた。

require 'thread'

class ThreadPool
  def initialize(size)
    @size = size
    @queue = Queue.new
    @threads = []
  end

  def execute(&block)
    @queue.push(block)
    @threads << create_thread if @threads.size < @size
  end

  def shutdown
    until @queue.num_waiting == @threads.size
      sleep(0.01)
    end
    @threads.each { |th| th.kill }
  end

  private
  def create_thread
    Thread.start(@queue) do |q|
      while true
        f = q.pop
        f.call
      end
    end
  end
end

if __FILE__ == $0
  pool = ThreadPool.new(3)
  10.times do |n|
    pool.execute { puts "#{Thread.current}: #{n}"; sleep(0.1) }
    pool.execute { puts "#{Thread.current}: #{n}"; sleep(0.1) }
  end
  pool.shutdown
end

やみくもにThreadを生成せずに使い回すことでRubyに優しくなります。
いつの間にか入ってる系のFastThreadを使うとより良いかも。
ThreadPoolを使えばいい感じなActor実装ができるかもしれない。RubyによるActor実装であるconcurrentはRubyに厳しすぎる。


ということでエコなプログラミングを心がけたいと思います。
クローラ書くときはsleep入れてサーバーにも優しくしよう。