0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Ruby で手続きを別プロセスに渡したい

Posted at

はじめに

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 などを使うとよいだろうか.

main.rb
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
worker.rb
# 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 にして,入力として渡してしまう方法だ.

main.rb
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
worker.rb
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 するという手もある.

main.rb
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
worker.rb
one_letter, pr = Marshal.load($stdin)
load(pr)
result = SomeClass.expensive_calculation(one_letter, &SomeProc)
Marshal.dump(result, $stdout)
pr.rb
SomeProc = Proc.new do |arg|
  # ...
end

pr.rb を別ファイルにすることになるので,ここの定義が大きくなる場合は,大きな String を作るよりは見通しが良くなる.小さい場合はわざわざ別ファイルにするのが面倒ではある.

だめな例

クラス,モジュールはなぜか Marshal.dump でエラーにならないが,インスタンスメソッドもクラスメソッドも Marshal.load で復元できないので意味がない.名前がついていれば大丈夫とわざわざ書いてある のだが,何が dump できるのかよくわからない.

おわりに

というわけで,Kernel.#evalKernel.#load で渡す方法があった.コードが小さいなら eval,大きいなら load を使って渡す(受け取る)のが良さそうだ.スコープの問題が発生しにくい方法としては,モジュールあたりの定義を渡すのがいいかもしれない.

  1. Ractor を使う手もあるかもしれないが,ちょっと触った限りではちゃんと書くのは結構大変そうだった.

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?