Enumerable#thread_withでRubyのスレッドを簡単に使おう!

前回の投稿ではsleep sortと
それに対抗したrunning sortを紹介したよ


sleep sortに対抗してrunning sortだ!(失敗に終わる編)


それらのアルゴリズムではRubyのThreadを使ったけど
Threadってなんか毎回書き方を忘れるよ
引数の受け渡し方とかjoinとか
もっと簡単にスレッディングしたいのにねぇ...


それでEnumerableなオブジェクトに対して
渡したブロックを並列処理してくれる
メソッドがあれば便利かなと考えたんだよ
それならスレッドの実装のことを忘れて
対象の処理のことだけ考えればいいからね



Enumerable#thread_withというメソッドを書いてみたよ*1

module Enumerable
  def thread_with
    mem = []
    map do |*item|
      Thread.new(*item) do |*_item|
        mem << yield(*_item)
      end
    end.each(&:join)
    mem
  end
end

Enumerable#thread_withがあれば
sleep sortは簡単だよ

a = (1..10).sort_by { rand } # => [9, 3, 10, 8, 4, 5, 7, 2, 6, 1]

a.thread_with { |i| sleep i } # => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]


効率を良くしたければ
次のようにすればいいし

a = (1..10).sort_by { rand } # => [9, 3, 10, 8, 4, 5, 7, 2, 6, 1]

a.thread_with { |i| sleep Math.log(i); i } # => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]


もう少し真面目な例を挙げるね


複数のWebサイトに並列的にアクセスして
そこからimgタグを拾ってくる例を示すよ
ここではベンチマークを取って
スレッドを使わない例と比べているよ

require "benchmark"
require "open-uri"

a = %w(www.nintendo.co.jp www.google.com www.yahoo.co.jp www.nikkei.com www.ruby-lang.org)
blk = ->item { open("http://#{item}").read.scan(/<img .*?>/) }

Benchmark.bmbm do |x|
  x.report { a.map(&blk)  }
  x.report { a.thread_with(&blk) }
end

# >> Rehearsal ------------------------------------
# >>    0.110000   0.040000   0.150000 (  1.112704)
# >>    0.020000   0.020000   0.040000 (  0.243304)
# >> --------------------------- total: 0.190000sec
# >> 
# >>        user     system      total        real
# >>    0.040000   0.010000   0.050000 (  0.756259)
# >>    0.020000   0.010000   0.030000 (  0.242218)

なんかThreadが身近になった感じがしない?


ただ先の実装には1つ問題があるよ
それはもとの配列の順位がthread_withの返り値として
保証されないことだよ*2

a = (1..1000).map { |i| i**2 }
b = (1..1000).thread_with { |i| i**2 }
a == b # => false


でも以下のようにすれば一応もとの順位は保証できるんだ

module Enumerable
  def thread_with(order=false)
    mem = []
    map.with_index do |*item, i|
      Thread.new(*item) do |*_item|
        mem << [i, yield(*_item)]
      end
    end.each(&:join)
    (order ? mem.sort : mem).map(&:last)
  end
end


a = (1..1000).map { |i| i**2 }
b = (1..1000).thread_with(true) { |i| i**2 }
a == b # => true

つまりthread_withが引数を取るようにして
trueを渡せば
最後にもとの配列の順位にソートしてくれる


まあ
たいしたネタじゃなかったね..

*1:まあThreadの使い方そのまんまなんだけど..

*2:まあそれがスレッドなんだけど