はじめに
Ruby の並列実行では,parallel を使うのが簡便である.
require 'parallel'
results = Parallel.map(['a','b','c'], in_processes: 3) do |one_letter|
SomeClass.expensive_calculation(one_letter)
end
他方,in_processes
では,メインプロセスをフォークして実行するので,メインプロセスのメモリ使用量が多く,分割プロセス数が膨大な場合,メモリが枯渇して Parallel::DeadWorker
や「Killed」が発生してしまう.これを回避するには,子プロセスを,Kernel.#system
などによる別コマンドとして実行すると良い1.入出力が必要な場合は,IO.popen
などを使うとよいだろうか.
require 'parallel'
results = Parallel.map(['a', 'b', 'c'], in_threads: 3) do |one_letter|
IO.popen("ruby #{File.join(__dir__, 'worker.rb')}", 'r+b') do |io|
Marshal.dump(one_letter, io)
next Marshal.load(io.read)
end
end
# SomeClass.expensive_calculation の定義
# ...
one_letter = Marshal.load($stdin)
result = SomeClass.expensive_calculation(one_letter)
Marshal.dump(result, $stdout)
ところで,これを自分用のライブラリの中で使っている場合,expensive_calculation に Proc
を渡したくなることがある.ところが,Proc
インスタンスやそれに類するものは Marshal
に対応していないため,単純には渡せない.どうしようか,というのが本題である.
解法例
Kernel.#eval
真っ先に思いついたのは Kernel.#eval
を使う方法.手続き定義部分を String
にして,入力として渡してしまう方法だ.
require 'parallel'
dfn = <<EOS
Proc.new do |arg|
# ...
end
EOS
results = Parallel.map(['a', 'b', 'c'], in_threads: 3) do |one_letter|
IO.popen("ruby #{File.join(__dir__, 'worker.rb')}", 'r+b') do |io|
Marshal.dump([one_letter, dfn], io)
next Marshal.load(io.read)
end
end
one_letter, dfn = Marshal.load($stdin)
pr = eval(dfn)
result = SomeClass.expensive_calculation(one_letter, &pr)
Marshal.dump(result, $stdout)
eval
は強力すぎるので,あんまり頼るのもなーという気持ちや,String
リテラル内に定義を書き込むのが気持ち悪いなという気持ちが少しある.ちなみに,Proc.new
だけでなく def
でメソッドを定義してもよい.定義の仕方と呼び出し方の対応が取れていればなんでもよいが,予めどれかに決める必要がある.
Kernel.#load
手続き定義部分を別ファイルにして,Kernel.#load
するという手もある.
require 'parallel'
results = Parallel.map(['a', 'b', 'c'], in_threads: 3) do |one_letter|
IO.popen("ruby #{File.join(__dir__, 'worker.rb')}", 'r+b') do |io|
Marshal.dump([one_letter, File.join(__dir__, 'pr.rb')], io)
next Marshal.load(io.read)
end
end
one_letter, pr = Marshal.load($stdin)
load(pr)
result = SomeClass.expensive_calculation(one_letter, &SomeProc)
Marshal.dump(result, $stdout)
SomeProc = Proc.new do |arg|
# ...
end
pr.rb を別ファイルにすることになるので,ここの定義が大きくなる場合は,大きな String
を作るよりは見通しが良くなる.小さい場合はわざわざ別ファイルにするのが面倒ではある.
だめな例
クラス,モジュールはなぜか Marshal.dump
でエラーにならないが,インスタンスメソッドもクラスメソッドも Marshal.load
で復元できないので意味がない.名前がついていれば大丈夫とわざわざ書いてある のだが,何が dump できるのかよくわからない.
おわりに
というわけで,Kernel.#eval
か Kernel.#load
で渡す方法があった.コードが小さいなら eval
,大きいなら load
を使って渡す(受け取る)のが良さそうだ.スコープの問題が発生しにくい方法としては,モジュールあたりの定義を渡すのがいいかもしれない.