1 自動的な並行性
Railsはさまざまな操作を同時に実行することを自動的に許可します。
デフォルトのPumaなどのスレッド型のWebサーバを使用する場合、複数のHTTPリクエストが同時に処理され、各リクエストには独自のコントローラインスタンスが提供されます。
組み込みのAsyncを含むスレッド型のActive Jobアダプタも同様に複数のジョブを同時に実行します。Action Cableのチャネルもこの方法で管理されます。
これらのメカニズムはすべて、各オブジェクト(コントローラ、ジョブ、チャネル)のユニークなインスタンスの作業を管理する複数のスレッドを含みますが、グローバルプロセススペース(クラスとその設定、グローバル変数など)を共有します。コードがこれらの共有されたものを変更しない限り、他のスレッドが存在することをほとんど無視できます。
このガイドの残りの部分では、Railsが「ほとんど無視できる」ようにするために使用するメカニズムと、特別なニーズを持つ拡張とアプリケーションがそれらを使用する方法について説明します。
2 エグゼキュータ
Railsエグゼキュータは、アプリケーションコードとフレームワークコードを分離します。フレームワークがアプリケーションで書かれたコードを呼び出すたびに、エグゼキュータによってラップされます。
エグゼキュータは2つのコールバックで構成されています:to_run
とto_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_run
とto_complete
のコールバックも提供します。これらはExecutorのコールバックと同じタイミングで呼び出されますが、現在の実行がアプリケーションのリロードを開始した場合にのみ呼び出されます。リロードが必要ない場合、Reloaderは他のコールバックなしでラップされたブロックを呼び出します。
3.2 クラスのアンロード
リロードプロセスの最も重要な部分は、クラスのアンロードです。ここでは、すべての自動読み込みクラスが削除され、再度読み込まれる準備ができます。これは、reload_classes_only_on_change
の設定に応じて、RunまたはCompleteコールバックの直前に発生します。
通常、クラスのアンロードの直前または直後に追加のリロードアクションを実行する必要があるため、Reloaderはbefore_class_unload
とafter_class_unload
のコールバックも提供します。
3.3 並行性
Reloaderは、長時間実行される「トップレベル」プロセスのみが呼び出すべきです。なぜなら、リロードが必要であると判断された場合、他のすべてのスレッドがExecutorの呼び出しを完了するまでブロックされるからです。
これが「子」スレッドで発生した場合、Executor内の待機中の親と一緒に、避けられないデッドロックが発生します。リロードは子スレッドが実行される前に行われる必要がありますが、親スレッドが実行中の場合に安全に実行することはできません。子スレッドは代わりにExecutorを使用する必要があります。
4 フレームワークの動作
Railsフレームワークのコンポーネントは、自身の並行性のニーズを管理するためにこれらのツールを使用します。
ActionDispatch::Executor
とActionDispatch::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_reloading
がtrue
であり、config.reload_classes_only_on_change
もtrue
である場合にのみファイルの変更をチェックします。これらはdevelopment
環境のデフォルトです。
config.enable_reloading
がfalse
の場合(デフォルトではproduction
)、ReloaderはExecutorへのパススルーになります。
Executorには常に重要な作業があります(たとえば、データベース接続の管理など)。config.enable_reloading
がfalse
であり、config.eager_load
がtrue
(production
のデフォルト)の場合、リロードは発生せず、ロードインターロックは必要ありません。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フォーラムで大歓迎です。