檢視原始碼 C 程式碼的自動讓步

簡介

Erlang NIFBIF 不應在沒有讓步的情況下執行太久(在 ERTS 的原始碼中通常稱為 trapping)。如果 NIF 和 BIF 佔用排程器執行緒太久,Erlang/OTP 系統將變得沒有回應,而且某些任務可能會不公平地獲得優先權。因此,最常用的、可能會執行很長時間的 NIF 和 BIF 可以讓步。

問題

Erlang NIF 和 BIF 通常以 C 程式語言實作。C 程式語言沒有內建支援在常式中間自動讓步(在其他程式語言中稱為 協程支援)。因此,大多數 NIF 和 BIF 都是手動實作讓步。手動實作讓步的優點是可以讓程式設計師控制應該儲存什麼以及何時應該讓步。不幸的是,手動實作讓步也會導致程式碼中出現大量樣板程式碼,這些程式碼比不讓步的程式碼更難以閱讀。此外,手動實作讓步可能既耗時又容易出錯,尤其是在 NIF 或 BIF 很複雜的情況下。

解決方案

為了更容易實作讓步的 NIF 和 BIF,建立了一個稱為 Yielding C Fun (YCF) 的源碼到源碼轉換器。YCF 是一個工具,它接收一組函式名稱和一個 C 原始碼檔案,並將原始碼檔案中具有給定名稱的函式轉換為可讓步的版本,這些版本可以用作協程。YCF 的建立是以讓步的 NIF 和 BIF 為目標,並且具有幾個在實作讓步的 NIF 和 BIF 時可能會很方便的功能。建議讀者查看 YCF 的文件,以取得 YCF 的詳細描述。

Yielding C Fun 的原始碼和文件

YCF 的原始碼包含在 Erlang/OTP 系統原始碼樹內的 "$ERL_TOP"/erts/lib_src/yielding_c_fun/ 資料夾中。YCF 的文件可以在 "$ERL_TOP"/erts/lib_src/yielding_c_fun/README.md 中找到。YCF 文件的渲染版本可以在這裡找到。

Erlang 執行時間系統中的 Yielding C Fun

在撰寫本文時,YCF 在 ERTS 中用於以下用途

ERTS 中使用 YCF 的最佳實務

首先,在開始使用 YCF 之前,建議閱讀其在 erts/lib_src/yielding_c_fun/README.md 中的文件,以了解 YCF 的限制和功能。

標記 YCF 轉換的函式

重要的是,要能夠很容易地看到哪些函式是由 YCF 轉換的,這樣編輯這些函式的程式設計師就會知道他們必須遵守某些限制。為了清楚起見,慣例是在函式上方加上註解,說明該函式是由 YCF 轉換的(例如,請參閱 erl_map.c 中的 maps_values_1_helper)。如果僅使用函式的轉換版本,則慣例是使用以下 #ifdef 將函式的原始碼「註解掉」(這樣,就不會收到關於未使用函式的警告)

#ifdef INCLUDE_YCF_TRANSFORMED_ONLY_FUNCTIONS
void my_fun() {
    ...
}
#endif /* INCLUDE_YCF_TRANSFORMED_ONLY_FUNCTIONS */

在編輯函式時,可以定義 INCLUDE_YCF_TRANSFORMED_ONLY_FUNCTIONS,以便可以看到非轉換原始碼中的錯誤和警告。

在哪裡放置 YCF 轉換的函式

慣例是將由 YCF 轉換的函式的非轉換原始碼放置在它們自然所屬的原始碼檔案中。例如,地圖 BIF 的函式與其他地圖相關的函式一起放置在 erl_map.c 中。在建置時,會呼叫 YCF 以將函式的轉換版本產生到標頭檔中,該標頭檔包含在包含函式非轉換版本的原始碼檔案中(在 $ERL_TOP/erts/emulator/Makefile.in 中搜尋 YCF 以查看如何呼叫 YCF 的範例)。

如果由一個 YCF 呼叫轉換的函式 F1 依賴於由另一個 YCF 呼叫轉換的函式 F2,則需要告訴 YCF F2 是一個 YCF 轉換的函式,以便 F1 可以呼叫轉換版本(有關如何執行此操作的更多資訊,請參閱 YCF 文件-fexternal 的文件)。

使用 erts_ycf_trap_driver 減少樣板程式碼

erts_ycf_trap_driver 是一個 C 函式,它實作了所有使用 YCF 進行讓步的 BIF 所需的通用程式碼。建議盡可能使用此函式。學習如何使用 erts_ycf_trap_driver 的一個好方法是查看 BIF maps:from_keys/2maps:from_list/1maps:keys/1maps:values/1 的實作。

某些 BIF 可能無法使用 erts_ycf_trap_driver,因為它們需要在讓步後執行一些自訂工作。例如,BIF ets:insert/2ets:insert_new/2 在 ETS 表格結構中發佈讓步狀態,以便其他執行緒可以協助完成操作。

測試和尋找 YCF 產生程式碼中的問題

測試具有手動讓步和 YCF 產生讓步的程式碼的一個好方法是編寫測試案例,涵蓋程式碼可以讓步的地方(讓步點),並設定讓步限制,以便在每次達到讓步點時都讓步。使用 YCF,可以透過將值 1 的指標作為 ycf_nr_of_reductions 參數傳遞(即,*_ycf_gen_yielding*_ycf_gen_continue 函式的第一個參數)來完成此操作。

YCF 標誌 -debug 會使 YCF 產生在讓步時檢查 C 堆疊指標的程式碼。當找到這樣的指標時,將會列印找到的指標的位置,並且程式將會崩潰。這可以在將現有的 C 程式碼移植到使用 YCF 讓步時節省大量時間!為了使 -debug 選項按預期工作,必須在呼叫 YCF 產生的函式之前告訴 YCF 堆疊的起始位置。建立函式 ycf_debug_set_stack_startycf_debug_reset_stack_start 是為了讓這更容易(有關如何使用這些函式,請參閱 erts_ycf_trap_driver 的實作)。建議設定 ERTS 的建置,以便 ERTS 的偵錯版本執行時,使用 -debug 標誌產生的 YCF 程式碼,而生產程式碼執行時,使用沒有 -debug 標誌產生的 YCF 程式碼。

一個好的做法是查看 YCF 產生的程式碼,以嘗試找出未正確轉換的內容。在執行此操作之前,應使用自動程式碼格式化工具來格式化產生的程式碼(否則產生的程式碼會很難閱讀)。如果 YCF 未正確轉換某些內容,幾乎可以肯定的是可以透過重寫程式碼來修復該問題(請參閱 YCF 文件,以了解支援和不支援的內容)。例如,如果您有內嵌結構變數宣告(例如,struct {int field1; int field2;} mystructvar;),YCF 將不會將其識別為變數宣告,但是您可以使用該結構建立 typedef 來修復此問題。

當偵錯由 YCF 轉換的程式碼時,YCF 的掛鉤可能會很有用。例如,這些掛鉤可用於在讓步時以及在讓步後恢復時列印變數的值。

不幸的是,YCF 對於語法錯誤的 C 程式碼處理效果不佳,並且在給定語法錯誤的 C 程式碼(例如,遺失的括號)時,可能會崩潰或產生不良的輸出,而不會給出任何有用的錯誤訊息。因此,建議在透過 YCF 轉換程式碼之前,始終使用正常的 C 編譯器檢查程式碼。

常見陷阱

  • 堆疊指標當讓步的函式繼續執行時,堆疊可能位於其他地方,因此指向位於堆疊上的變數的指標可能會成為問題。如上一節所述,-debug 選項是偵測此類指標的好方法。YCF 具有使移植具有堆疊指標的程式碼更容易的功能(有關更多資訊,請參閱 YCF 文件中 YCF_STACK_ALLOC 的文件)。修復堆疊指標的另一種方法(有時可能很方便)是在讓步的函式恢復時,使用 YCF 的掛鉤來正確設定堆疊的指標。

  • 巨集 YCF 不會展開巨集,因此被巨集「隱藏」的變數宣告、return 語句和 goto 等可能會成為問題。因此,檢查由 YCF 轉換的程式碼中的所有巨集,以確保它們不包含任何 YCF 需要轉換的內容,這是明智之舉。

  • 讓步程式碼中的記憶體配置 如果在執行讓步的 BIF 時終止進程,則必須確保釋放由讓步程式碼配置的記憶體和其他資源。例如,可以透過從保持對 trap 狀態參考的 magic binary 的 dtor 呼叫產生的 *_ycf_gen_destroy 函式來完成此操作。當執行函式的 *_ycf_gen_destroy 函式時,可以使用 YCF 的 ON_DESTROY_STATEON_DESTROY_STATE_OR_RETURN 掛鉤來釋放在讓步函式內部手動配置的任何資源。erts_ycf_trap_driver 會負責呼叫 *_ycf_gen_destroy 函式,因此如果您使用的是 erts_ycf_trap_driver,則無需擔心這一點。