v7.0.0
更多資訊請前往 rubyonrails.org: 更多在 Ruby on Rails

Rails 中的執行緒和程式碼執行

閱讀本指南後,您將瞭解:

1 自動平行計算

Rails 自動允許同時執行各種操作。

當使用執行緒 web 伺服器時,比如預設的 Puma,多個 HTTP 請求將同時提供,每個請求都提供自己的 controller 實例。

執行緒的 Active Job 介面卡,包括內建的 Async,同樣會 同時執行多個作業。 Action Cable 頻道由這個管理 方式也是。

這些機制都涉及多個執行緒,每個執行緒管理一個獨特的工作 某個物件的實例(controller、job、channel),同時共享全域性 程序空間(例如類及其設定和全域性變數)。 只要你的程式碼不修改任何這些共享的東西,它基本上可以 忽略其他執行緒的存在。

本指南的其餘部分描述了 Rails 用來使其“主要是 ignorable”,以及具有特殊需求的擴充套件和應用程式如何使用它們。

2 執行者

Rails Executor 將應用程式程式碼與框架程式碼分開:任何時候 框架呼叫您在應用程式中編寫的程式碼,它將被包裝 執行者。

Executor 由兩個 callbacks 組成:to_runto_complete。執行 callback在應用程式碼之前呼叫,完整的callback是 叫後。

2.1 預設 callbacks

在預設的 Rails 應用程式中,Executor callbacks 用於:

  • track 哪些執行緒處於自動載入和重新載入的安全位置
  • 啟用和禁用 Active Record 查詢快取
  • 將獲得的 Active Record 連線返回到池中
  • 限制內部快取壽命

在 Rails 5.0 之前,其中一些由單獨的 Rack 中介軟體處理 類(例如 ActiveRecord::ConnectionAdapters::ConnectionManagement),或 直接用類似的方法包裝程式碼 ActiveRecord::Base.connection_pool.with_connection。執行者替換 這些帶有一個更抽象的介面。

2.2 包裝應用程式程式碼

如果您正在編寫將呼叫應用程式程式碼的庫或元件,您 應該通過呼叫 executor 來包裝它:

Rails.application.executor.wrap do
  # call application code here
end

提示:如果您從長時間執行的程序中重複呼叫應用程式程式碼, 可能想要使用 Reloader 來包裝。

每個執行緒都應該在它執行應用程式程式碼之前被包裝,所以如果你 應用程式手動將工作委託給其他執行緒,例如通過 Thread.new 或使用執行緒池的平行計算 Ruby 功能,您應該立即包裝 塊:

Thread.new do
  Rails.application.executor.wrap do
    # your code here
  end
end

平行計算 Ruby 使用 ThreadPoolExecutor,它有時會設定 帶有 executor 選項。儘管名稱,它是無關的。

Executor 是安全可重入的;如果它已經在當前處於活動狀態 執行緒,wrap 是空操作。

如果將應用程式程式碼包裝在一個塊中是不切實際的(對於 例如,Rack API 使這有問題),您也可以使用 run! / complete! 對:

Thread.new do
  execution_context = Rails.application.executor.run!
  # your code here
ensure
  execution_context.complete! if execution_context
end

2.3 平行計算

Executor 會在Load 互鎖。此操作將暫時阻塞,如果另一個 執行緒當前正在自動載入常量或解除安裝/重新載入 應用程式。

3 重灌器

與 Executor 一樣,Reloader 也包裝應用程式程式碼。如果執行者是 尚未在當前執行緒上處於活動狀態,重新載入器將為您呼叫它, 所以你只需要呼叫一個。這也保證了 Reloader 的一切 確實,包括其所有的 callback 呼叫,發生在包裹在 執行者。

Rails.application.reloader.wrap do
  # call application code here
end

Reloader 只適用於長時間執行的框架級程序 重複呼叫應用程式程式碼,例如 Web 伺服器或作業佇列。 Rails 自動包裝網路請求和 Active Job 工作人員,所以你很少 需要自己呼叫 Reloader。始終考慮 Executor 是否 更適合您的用例。

3.1 Callbacks

在進入包裹塊之前,Reloader 會檢查是否正在執行 應用程式需要重新載入——例如,因為 model 的原始檔有 被修改。如果它確定需要重新載入,它將等到它 安全,然後在繼續之前這樣做。當應用程式設定為 無論是否檢測到任何更改,始終重新載入,重新載入是 而是在塊的末尾執行。

Reloader 還提供了 to_runto_complete callbacks;他們是 在與 Executor 相同的點呼叫,但僅噹噹前 執行已啟動應用程式重新載入。當認為沒有重新載入時 必要時,重新載入器將呼叫沒有其他 callbacks 的包裝塊。

3.2 類解除安裝

過載過程中最重要的部分是類解除安裝,其中 所有自動載入的類都被刪除,準備再次載入。這會發生 緊接在 Run 或 Complete callback 之前,具體取決於 reload_classes_only_on_change 設定。

通常,額外的重新載入 actions 需要在之前或之前執行 就在 Class Unload 之後,所以 Reloader 也提供了 before_class_unloadafter_class_unload callbacks。

3.3 平行計算

只有長時間執行的“頂級”程序才應該呼叫 Reloader,因為如果 它確定需要重新載入,它將阻塞,直到所有其他執行緒都 完成了任何 Executor 呼叫。

如果這發生在“子”執行緒中,並且在“子”執行緒中有一個等待的父執行緒 Executor,它會導致一個不可避免的死鎖:reload 必須發生在 子執行緒已執行,但在父執行緒期間無法安全執行 執行緒正在執行中。子執行緒應該使用 Executor 代替。

4 框架行為

Rails 框架元件使用這些工具來管理自己的平行計算 也需要。

ActionDispatch::ExecutorActionDispatch::Reloader 是 Rack 中介軟體 分別使用提供的 Executor 或 Reloader 包裝請求。他們 自動包含在預設應用程式堆疊中。重灌器將 確保任何到達的 HTTP 請求都帶有新載入的 應用程式,如果發生任何程式碼更改。

Active Job 還使用 Reloader 包裝其作業執行,載入最新的 當每個作業離開佇列時執行它的程式碼。

Action Cable 使用 Executor 代替:因為電纜連線連結到 一個類的特定實例,不可能每次到達都重新載入 WebSocket 訊息。但是,只有訊息處理程式被包裝;一個長期執行 電纜連線不會阻止由新傳入觸發的重新載入 請求或工作。相反,Action Cable 使用 Reloader 的 Action Cable callback 斷開其所有連線。當客戶端自動 重新連線,它將與新版本的程式碼對話。

以上是框架的入口點,所以他們負責 確保它們各自的執行緒受到保護,並決定是否重新載入 有必要的。其他元件只需要在產生時使用 Executor 附加執行緒。

4.1 設定

Reloader 僅在 cache_classes 為 false 時檢查檔案更改,並且 reload_classes_only_on_change 為真(這是 development 環境)。

cache_classes 為真時(在 production 中,預設情況下),Reloader 只有 傳遞給 Executor。

Executor 總是有重要的工作要做,比如資料庫連線 管理。當 cache_classeseager_load 都為真(production)時, 不會發生自動載入或類重新載入,因此不需要 Load 互鎖。如果其中任何一個為 false (development),則 Executor 將 使用載入聯鎖確保常量僅在安全時載入。

5 負載聯鎖

裝載聯鎖允許自動裝載和重新裝載 多執行緒執行時環境。

當一個執行緒通過評估類定義執行自動載入時 從適當的檔案中,重要的是沒有其他執行緒遇到 對部分定義的常量的引用。

同樣,只有在沒有應用程式程式碼時執行解除安裝/重新載入才是安全的 is in mid-execution: 在重新載入後,例如 User 常量,可能 指向不同的類。如果沒有這條規則,不合時宜的重新載入將意味著 User.new.class == User,甚至 User == User,都可能是假的。

這兩個約束都由負載聯鎖解決。它保持 track 的 哪些執行緒當前正在執行應用程式程式碼、載入類,或 解除安裝自動載入的常量。

一次只能載入或解除安裝一個執行緒,並且要執行任一操作,它必須等待 直到沒有其他執行緒正在執行應用程式程式碼。如果一個執行緒正在等待 執行載入,它不會阻止其他執行緒載入(實際上,它們會 合作,並在所有恢復之前依次執行他們排隊的載入 一起跑)。

5.1 permit_concurrent_loads

Executor 在其執行期間自動獲取 running 鎖 阻止,並且自動載入知道何時升級到 load 鎖,並切換回 之後再次 running

在 Executor 塊內執行的其他阻塞操作(包括 所有應用程式程式碼),然而,可以不必要地保留 running 鎖。如果 另一個執行緒遇到一個必須自動載入的常量,這可能導致 僵局。

例如,假設 User 還沒有載入,下面會死鎖:

Rails.application.executor.wrap do
  th = Thread.new do
    Rails.application.executor.wrap do
      User # inner thread waits here; it cannot load
           # User while another thread is running
    end
  end

  th.join # outer thread waits here, holding 'running' lock
end

為了防止這種死鎖,外執行緒可以使用permit_concurrent_loads。經過 呼叫此方法,執行緒保證它不會取消引用任何 可能在提供的塊內自動載入常量。最安全的見面方式 這個承諾是儘可能接近阻塞呼叫:

Rails.application.executor.wrap do
  th = Thread.new do
    Rails.application.executor.wrap do
      User # inner thread can acquire the 'load' lock,
           # load User, and continue
    end
  end

  ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
    th.join # outer thread waits here, but has no lock
  end
end

另一個例子,使用平行計算 Ruby:

Rails.application.executor.wrap do
  futures = 3.times.collect do |i|
    Concurrent::Future.execute do
      Rails.application.executor.wrap do
        # do work here
      end
    end
  end

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

5.2 ActionDispatch::DebugLocks

如果您的應用程式死鎖並且您認為 Load Interlock 可能是 涉及到的,可以臨時新增ActionDispatch::DebugLocks中介軟體 config/application.rb

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

如果您隨後重新啟動應用程式並重新觸發死鎖條件, /rails/locks 將顯示當前已知的所有執行緒的摘要 互鎖,他們持有或等待的鎖定級別,以及他們當前的 回溯。

通常死鎖是由互鎖與其他一些衝突引起的 外部鎖定或阻塞 I/O 呼叫。一旦你找到它,你可以用它包裹它 permit_concurrent_loads

回饋

我們鼓勵您幫助提高本指南的品質。

如果您發現任何拼寫錯誤或資訊錯誤,請提供回饋。 要開始回饋,您可以閱讀我們的 回饋 部分。

您還可能會發現不完整的內容或不是最新的內容。 請務必為 main 新增任何遺漏的文件。假設是 非穩定版指南(edge guides) 請先驗證問題是否已經在主分支上解決。 請前往 Ruby on Rails 指南寫作準則 查看寫作風格和慣例。

如果由於某種原因您發現要修復的內容但無法自行修補,請您 提出 issue

關於 Ruby on Rails 的任何類型的討論歡迎提供任何文件至 rubyonrails-docs 討論區