# 非同期 * Tatsuhiro Ujihisa * ## 一般的な話 * 非同期とは? * スレッド、プロセス * プロセス生成 * system, IO.popen, Open3.popen3, fork ## 非同期とは? * "非"非同期とはすなわち逐次処理。上から下へ一本の処理が進む * 非同期の場合、複数の処理が同時に進む ## スレッドとプロセス * Rubyを立ち上げると、OSが管理するプロセスが一つ立ち上がる * Rubyでスレッドを作ると、Rubyプロセスの中で仮想的に処理の流れが増える * Rubyでプロセスを作ると、OSが管理するプロセス自体が増える ## スレッドの作り方 Thread.start do something end ## プロセスの作り方 * systemで`&`をつける * IO.popenする * forkする それぞれ詳しく解説すると、 ## systemで& system 'ls &' * 簡単に外部コマンドを非同期実行 * 出力をRubyで受け取れない * プロセスID取得不可能 * $?は"sh -c 'ls &'"のプロセスID * Windowsで動かない ## IO.popen IO.popen('ls') {|io| io.gets * 出力をRubyで受け取れる! * プロセスID取得可能! (`io.pid`) * 標準エラー出力はそのままターミナルに出てしまう ## Open3.popen3 Open3.popen3('ls') {|i, o, e * 標準エラー出力も取得可能! * でもプロセスIDが取得できない... * 深い事情がある ## fork fork { something } * 起動中のrubyプロセス自体をコピーして、ブロックを非同期に実行 * プロセスIDはブロックの返り値 * ブロック内は別世界。便利! * Windowsで動かない ## Threadとforkの違い Thread.start { $a = 1 } fork { $a = 1 } * スレッドの場合元の`$a`が変化する。 * forkの場合変化しない。 ## exec = system + exit exec 'ls' p 'this message will never show' * 実行中のプロセス自体がexecの引数のコマンドになる * forkと組み合わせると...!! ## 具体的な話 具体的な問題と、その解決方法を考えよう! ## 例題1: Sinatraを動かして、終了したい 解法1: Threadとsystem t = Thread.start do system 'ruby aaa.rb' end # something t.kill! ダメ * `t.kill!`でSinatraが終了しない * 元のRubyが終了してもSinatraが生き残る。psしてkill 解法2: Threadとsystem, ただしシェル経由しない t = Thread.start do system 'ruby', 'aaa.rb' end # something t.kill! ダメ * Thread死すともsystem死なず 解法3: IO.popen io = IO.popen('ruby aaa.rb') # something Process.kill 'KILL', io.pid 惜しい * きちんとSinatraを終了できる! * でも大量なアクセスログが 解法4: Open3.popen3 stdin, stdout, stderr = Open3.popen3('ruby', 'aaa.rb') # something Process.kill 'KILL', io.pid...? もっとダメ * アクセスログは隠せる * でもプロセスIDが分からず、終了できない... 解法5: forkとsystem pid = fork { system 'ruby', 'aaa.rb' } # something Process.kill 'KILL', pid ぜんぜんダメ * 終了できない "fork死すともsystem死なず" * 出力隠せない 解法6: forkとexec pid = fork do exec 'ruby, 'aaa.rb' end # something Process.kill 'KILL', pid 惜しい * 終了できる! * 出力隠せない 解法7: 出力先を変えてからexec pid = fork do STDERR.reopen File.open('/dev/null', 'w') exec 'ruby, 'aaa.rb' end # something Process.kill 'KILL', pid できた! * ただ、forkはWindowsで動かない 解法8: spawnを使う (new!) pid = spawn 'ruby', 'aaa.rb', :out => '/dev/null' # something Process.kill 'KILL', pid * ruby 1.9から利用可能 * 便利。簡潔。 ## 整理 * ruby 1.9 + UNIX: 最強。ラクなspawnか、面倒なfork+exec * ruby 1.8 + UNIX: ちょっと面倒だけどfork+exec * ruby 1.9 + Windows: spawnがあるから問題なし * ruby 1.8 + Windows: あ... (win32-open4という拡張ライブラリで一応解決) ## 実際のところ * Macならば最初から1.8が入っているのでそれを使いたい * WinはそもそもRubyが入っていないので1.9を入れさせればいい * 一つのコードをMacとWinでコンパチブルにしたければ? * fork+execとspawnの両方で書いてプラットフォームで分岐しないといけない * バグの温床 ## 解決! Pure Rubyなspawn実装 gem install sfl * Spawn for Legacy * 作者: ujihisa ## 使い方 require 'rubygems' require 'sfl' * これでspawnが定義される * ほとんどruby 1.9のspawnとコンパチブル ## 使用例 pid = spawn( {"GIT_EXEC_PATH" => "/usr/bin"}, 'git', 'pull', 'origin', 'master', [:out, :err] => ['/tmp/log.txt', 'a'], :chdir => '~/git/aaa') sleep 10 Process.kill 'KILL', pid * 環境変数指定、コマンド、リダイレクト、カレントディレクトリ * 全てruby 1.9標準添付メソッドspawn準拠 ## まとめ * Thread.start, system, exec, fork, IO.popen, Open3.popen3, spawn, ... * spawnが一番便利 * ruby 1.8でもspawn利用可能 おわり ## 参考文献 * * * TokyoRubyKaigiのakrさんのspawn, open3に関するプレゼン (!) ## 時間的な都合で削ったもの * ruby 1.9のopen3は大幅書き直しですごい便利 * spawnを使っている * pid取得可能。実質open4 * sflでopen3を動かしたい * そのままでは動かないが、open3を少し書き換えれば動くはず * 開発中 * gemのopen4という選択肢も * 互換性なし * windowsで動かないがopen4拡張ならOK * 実はopen系ライブラリは乱発している * `gem search open -r` * 本当に必要なには外側のopenではなく基盤のspawn * pure ruby実装のspawnがあると思ったが探しても見つからなかったので自作 * みんなspawn知らない? * pure rubyであることのうまみ * forkとexecがあればどこでも動く。JRuby, MacRuby, Rubinius! * マルチプラットフォーム対応しやすい (まさに必要だった)