edge
詳細はrubyonrails.orgで: もっとRuby on Rails

Railsでのスレッドとコードの実行

このガイドを読むことで、以下のことがわかります:

1 自動的な並行性

Railsはさまざまな操作を同時に実行することを自動的に許可します。

デフォルトのPumaなどのスレッド型のWebサーバを使用する場合、複数のHTTPリクエストが同時に処理され、各リクエストには独自のコントローラインスタンスが提供されます。

組み込みのAsyncを含むスレッド型のActive Jobアダプタも同様に複数のジョブを同時に実行します。Action Cableのチャネルもこの方法で管理されます。

これらのメカニズムはすべて、各オブジェクト(コントローラ、ジョブ、チャネル)のユニークなインスタンスの作業を管理する複数のスレッドを含みますが、グローバルプロセススペース(クラスとその設定、グローバル変数など)を共有します。コードがこれらの共有されたものを変更しない限り、他のスレッドが存在することをほとんど無視できます。

このガイドの残りの部分では、Railsが「ほとんど無視できる」ようにするために使用するメカニズムと、特別なニーズを持つ拡張とアプリケーションがそれらを使用する方法について説明します。

2 エグゼキュータ

Railsエグゼキュータは、アプリケーションコードとフレームワークコードを分離します。フレームワークがアプリケーションで書かれたコードを呼び出すたびに、エグゼキュータによってラップされます。

エグゼキュータは2つのコールバックで構成されています:to_runto_completeです。Runコールバックはアプリケーションコードの前に呼び出され、Completeコールバックは後に呼び出されます。

2.1 デフォルトのコールバック

デフォルトのRailsアプリケーションでは、エグゼキュータのコールバックは以下のように使用されます:

  • 自動ロードとリロードのために安全な位置にあるスレッドを追跡する
  • Active Recordのクエリキャッシュを有効化および無効化する
  • 取得したActive Recordの接続をプールに戻す
  • 内部キャッシュの寿命を制約する

Rails 5.0より前では、これらのいくつかは別々のRackミドルウェアクラス(ActiveRecord::ConnectionAdapters::ConnectionManagementなど)で処理されたり、ActiveRecord::Base.connection_pool.with_connectionのようなメソッドでコードを直接ラップしたりしました。エグゼキュータはこれらをより抽象的な単一のインターフェースに置き換えます。

2.2 アプリケーションコードのラップ

アプリケーションコードを呼び出すライブラリやコンポーネントを作成する場合は、エグゼキュータの呼び出しでそれをラップする必要があります:

Rails.application.executor.wrap do
  # ここでアプリケーションコードを呼び出す
end

長時間実行されるプロセスから繰り返しアプリケーションコードを呼び出す場合は、リローダーを使用してラップすることを検討してください。

各スレッドは、アプリケーションコードを実行する前にラップする必要があります。したがって、アプリケーションが手動でThread.newやスレッドプールを使用して他のスレッドに作業を委任する場合は、すぐにブロックをラップする必要があります:

Thread.new do
  Rails.application.executor.wrap do
    # ここにコードを記述
  end
end

注意:Concurrent RubyはThreadPoolExecutorを使用し、executorオプションで構成することがありますが、名前は関係ありません。

エグゼキュータは安全に再入可能です。現在のスレッドで既にアクティブな場合、wrapは何もしません。

ブロックでアプリケーションコードをラップすることが実用的ではない場合(たとえば、Rack APIではこれが問題になる場合)、run! / complete!のペアを使用することもできます:

Thread.new do
  execution_context = Rails.application.executor.run!
  # ここにコードを記述
ensure
  execution_context.complete! if execution_context
end

2.3 並行性

エグゼキュータは、現在のスレッドをロードインターロックの「実行中」モードにします。この操作は、別のスレッドが現在定数を自動ロードしているか、アプリケーションをアンロード/リロードしている場合に一時的にブロックされます。

3 リローダー

エグゼキュータと同様に、リローダーもアプリケーションコードをラップします。エグゼキュータが現在のスレッドで既にアクティブでない場合、リローダーは自動的にそれを呼び出すため、1つだけ呼び出す必要があります。これにより、リローダーが行うすべてのこと、すべてのコールバックの呼び出しなどが、エグゼキュータの内部でラップされることが保証されます。

Rails.application.reloader.wrap do
  # ここでアプリケーションコードを呼び出す
end

リローダーは、Webサーバやジョブキューなどの長時間実行されるフレームワークレベルのプロセスが繰り返しアプリケーションコードを呼び出す場合にのみ適しています。RailsはWebリクエストとActive Jobワーカーを自動的にラップするため、自分自身でリローダーを呼び出す必要はほとんどありません。使用ケースによっては、エグゼキュータの方が適しているかどうかを常に考慮してください。

3.1 コールバック

ラップされたブロックに入る前に、リローダーは実行中のアプリケーションがリロードが必要かどうかをチェックします。たとえば、モデルのソースファイルが変更された場合などです。リロードが必要であると判断された場合、安全な状態になるまで待機し、その後続行する前にリロードを行います。アプリケーションが変更が検出されたかどうかに関係なく常にリロードするように設定されている場合、リロードはブロックの最後で実行されます。 Reloaderは、to_runto_completeのコールバックも提供します。これらはExecutorのコールバックと同じタイミングで呼び出されますが、現在の実行がアプリケーションのリロードを開始した場合にのみ呼び出されます。リロードが必要ない場合、Reloaderは他のコールバックなしでラップされたブロックを呼び出します。

3.2 クラスのアンロード

リロードプロセスの最も重要な部分は、クラスのアンロードです。ここでは、すべての自動読み込みクラスが削除され、再度読み込まれる準備ができます。これは、reload_classes_only_on_changeの設定に応じて、RunまたはCompleteコールバックの直前に発生します。

通常、クラスのアンロードの直前または直後に追加のリロードアクションを実行する必要があるため、Reloaderはbefore_class_unloadafter_class_unloadのコールバックも提供します。

3.3 並行性

Reloaderは、長時間実行される「トップレベル」プロセスのみが呼び出すべきです。なぜなら、リロードが必要であると判断された場合、他のすべてのスレッドがExecutorの呼び出しを完了するまでブロックされるからです。

これが「子」スレッドで発生した場合、Executor内の待機中の親と一緒に、避けられないデッドロックが発生します。リロードは子スレッドが実行される前に行われる必要がありますが、親スレッドが実行中の場合に安全に実行することはできません。子スレッドは代わりにExecutorを使用する必要があります。

4 フレームワークの動作

Railsフレームワークのコンポーネントは、自身の並行性のニーズを管理するためにこれらのツールを使用します。

ActionDispatch::ExecutorActionDispatch::Reloaderは、リクエストを提供されたExecutorまたはReloaderでラップするRackミドルウェアです。これらはデフォルトのアプリケーションスタックに自動的に含まれます。Reloaderは、コードの変更が発生した場合、到着したHTTPリクエストが最新のコピーのアプリケーションで提供されることを保証します。

Active JobもReloaderでジョブの実行をラップし、キューからジョブが取り出されるたびに最新のコードをロードします。

Action Cableは代わりにExecutorを使用します。Cable接続は特定のクラスのインスタンスにリンクされているため、到着するWebSocketメッセージごとにリロードすることはできません。ただし、メッセージハンドラのみがラップされます。長時間実行されるCable接続は、新しい着信リクエストやジョブによってトリガされるリロードを防ぎません。代わりに、Action CableはReloaderのbefore_class_unloadコールバックを使用してすべての接続を切断します。クライアントが自動的に再接続すると、新しいバージョンのコードと通信することになります。

上記はフレームワークへのエントリーポイントであるため、それぞれのスレッドが保護され、リロードが必要かどうかを決定する責任があります。他のコンポーネントは、追加のスレッドを生成する場合にのみExecutorを使用する必要があります。

4.1 設定

Reloaderは、config.enable_reloadingtrueであり、config.reload_classes_only_on_changetrueである場合にのみファイルの変更をチェックします。これらはdevelopment環境のデフォルトです。

config.enable_reloadingfalseの場合(デフォルトではproduction)、ReloaderはExecutorへのパススルーになります。

Executorには常に重要な作業があります(たとえば、データベース接続の管理など)。config.enable_reloadingfalseであり、config.eager_loadtrueproductionのデフォルト)の場合、リロードは発生せず、ロードインターロックは必要ありません。development環境のデフォルト設定では、Executorはロードインターロックを使用して、定数が安全にロードされるようにします。

5 ロードインターロック

ロードインターロックは、マルチスレッドのランタイム環境で自動読み込みとリロードを有効にするための仕組みです。

1つのスレッドが適切なファイルからクラス定義を評価して自動読み込みを実行している場合、他のスレッドが部分的に定義された定数への参照に遭遇しないようにすることが重要です。

同様に、アプリケーションコードが実行中でない場合にのみアンロード/リロードを実行することが安全です。リロード後、例えばUser定数は異なるクラスを指す可能性があります。このルールがないと、タイミングの悪いリロードにより、User.new.class == UserまたはUser == Userがfalseになる可能性があります。

これらの制約は、ロードインターロックによって解決されます。ロードインターロックは、現在アプリケーションコードを実行しているスレッド、クラスのロード、または自動読み込みされた定数のアンロードを追跡します。

1度に1つのスレッドのみがロードまたはアンロードを実行でき、どちらを行うにしても、他のスレッドがアプリケーションコードを実行していないことを待たなければなりません。ロードを実行するために待機しているスレッドは、他のスレッドのロードを妨げません(実際には、協力して、順番にロードを実行し、すべてが一緒に再開します)。

5.1 permit_concurrent_loads

Executorは、ブロックの実行中に自動的にrunningロックを取得し、autoloadはloadロックにアップグレードし、その後再びrunningに切り替えるタイミングを知っています。 ただし、Executorブロック内で実行される他のブロッキング操作は、不必要にrunningロックを保持する可能性があります。もう1つのスレッドが自動ロードする必要がある定数に遭遇した場合、これはデッドロックを引き起こす可能性があります。

たとえば、Userがまだロードされていないと仮定すると、次のようにデッドロックが発生します。

Rails.application.executor.wrap do
  th = Thread.new do
    Rails.application.executor.wrap do
      User # 内部スレッドはここで待機します; 他のスレッドが実行中の間にUserをロードすることはできません
    end
  end

  th.join # 外部スレッドはここで待機し、'running'ロックを保持します
end

このデッドロックを防ぐために、外部スレッドはpermit_concurrent_loadsを呼び出すことができます。このメソッドを呼び出すことで、スレッドは提供されたブロック内で可能性のある自動ロードされた定数を参照解除しないことを保証します。その約束を守る最も安全な方法は、ブロッキング呼び出しにできるだけ近い場所にそれを配置することです。

Rails.application.executor.wrap do
  th = Thread.new do
    Rails.application.executor.wrap do
      User # 内部スレッドは'load'ロックを取得し、Userをロードして続行できます
    end
  end

  ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
    th.join # 外部スレッドはここで待機しますが、ロックはありません
  end
end

Concurrent Rubyを使用した別の例:

Rails.application.executor.wrap do
  futures = 3.times.collect do |i|
    Concurrent::Promises.future do
      Rails.application.executor.wrap do
        # ここで作業を行う
      end
    end
  end

  values = ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
    futures.collect(&:value)
  end
end

5.2 ActionDispatch::DebugLocks

アプリケーションがデッドロックしており、Load Interlockが関与している可能性がある場合は、config/application.rbに一時的にActionDispatch::DebugLocksミドルウェアを追加できます。

config.middleware.insert_before Rack::Sendfile,
                                  ActionDispatch::DebugLocks

その後、アプリケーションを再起動し、デッドロックの状態を再度トリガーすると、/rails/locksには現在インターロックによって知られているすべてのスレッドの概要が表示され、それらが保持または待機しているロックレベルと現在のバックトレースが表示されます。

一般的には、デッドロックはインターロックが他の外部ロックまたはブロッキングI/O呼び出しと競合していることによって引き起こされます。見つけたら、permit_concurrent_loadsでそれをラップすることができます。

フィードバック

このガイドの品質向上にご協力ください。

タイポや事実の誤りを見つけた場合は、ぜひ貢献してください。 開始するには、ドキュメントへの貢献セクションを読んでください。

不完全なコンテンツや最新でない情報も見つかるかもしれません。 メインのドキュメントに不足しているドキュメントを追加してください。 修正済みかどうかは、まずEdge Guidesを確認してください。 スタイルと規約については、Ruby on Rails Guides Guidelinesを確認してください。

修正すべき点を見つけたが、自分で修正できない場合は、 問題を報告してください

そして最後に、Ruby on Railsのドキュメントに関するあらゆる議論は、公式のRuby on Railsフォーラムで大歓迎です。