QA@IT

Railsでアクションをバックグランドタスク的にthreadにしてもオッケー?

4500 PV

Railsであるアクションが3~10秒程度かかっています。こんな感じです。

def process
  Model.process(params[:item])  # これがio的に重たい処理
  flash.now[:success] = "Done!"
  render 'index'
end

これをdelayed_jobのようなバックグランド処理用のgemを使わずに、

def process
  Thread.new { Model.process(params[:item]) }
  render 'index'
end

としてしまって良いものでしょうか? 非同期バックグランド処理終了時に特にユーザー側にメッセージを出す必要はありませんが、終わった段階で出せると、なお良いかも。

回答

上記の書き方だとModel.processの処理が終了する前にアクションを抜けてしまうので駄目だとおもいます。

試しに以下のようなコードを使って試したところ「hello」しか出力してくれませんでした。

## Some controller
class SomeController < ApplicationController
  def index
    Thread.new { Rails.logger.info 'hello'; sleep 3; Rails.logger.info 'world' }

以前Merbにはrun_laterというメソッドが似たような機能を提供していて、それのRailsのポートの中でQueueとThreadが使われているようです。

https://github.com/jkraemer/run_later/blob/master/lib/run_later/worker.rb

自分で以下のようなのを実装したところ「world」まで出力してくれました。

## Inside Initializer

@@queue = Queue.new
Thread.new do
  while q = @@queue.pop
    q.call
  end
end

## Some controller
class SomeController < ApplicationController
  def index
    @@queue.push Proc.new{ Rails.logger.info 'hello'; sleep 3; Rails.logger.info 'world' }

ただこんなナイーブな実装だといろいろ問題があると思うので素直にdelayed_jobを使うのが良いかと思います。
あとThreadの詳しい使い方はdRuby本に一章まるごとあるのでそれが非常に参考になると思います。

「終わった段階でメッセージを出す」に関しては、それだと終わった段階までviewを描画できなくなってしまうのでThreadを使わないバージョンとあまり変わらなくなってしまいます。もし同時に立ち上げるプロセス数を少なくするのが目的であれば、Railsをthinのmultithreadバージョンで立ち上げる、jRubyを使う、あるいはasync-railsのようなものを使うのはどうでしょうか?私はどれも使ったことはありませんが。
あるいは一旦viewを描画させてしまって、クライアントサイドからajax-pollingやwebsocketをつかってタスク終了を通知させるというのも手です。

編集 履歴 (0)
  • 詳細にありがとうございます。やはり素直にdelayed_jobにしたほうがいいのでしょうか。Threadは排他制御やjoinのタイミングを気にせずに使えるのかと思いましたが、ダメなんですね。というか挙動がよく分かっていません。うーん -
  • Rails 4 でQueueがビルトインになりそうな気配です。

    https://github.com/rails/rails/commit/adff4a706a5d7ad18ef05303461e1a0d848bd662

    -

HerokuならThinなのでEventMachine使うのが鉄板だと思いますが、簡単なものならプロセスをspawnしてdetachするという手もあります。

(pid = fork) ? Process.detach(pid) : exec("do_something --heavy")
編集 履歴 (0)

Thin をお使いであれば、EventMachine を使うという手もあります。

def index
  EM.defer do
    Rails.logger.info 'hello'
    sleep 3
    Rails.logger.info 'world'
  end

  render text: 'hi'
end

(余談ですが、process という名前のアクションは ActionController と衝突するので避けた方が良さそうです)

編集 履歴 (1)
  • なるほど、やってみます! -
  • EventMachineで、うまい具合に非同期処理が実現できて、体感速度の上では劇的に効果がありました。ありがとうございます! -
ウォッチ

この質問への回答やコメントをメールでお知らせします。