Wasm Workers API

Wasm Workers API 使 C/C++ 程式碼能夠利用 Web Workers 和共用的 WebAssembly.Memory (SharedArrayBuffer) ,透過直接的網頁式程式設計 API 建構多執行緒程式。

快速範例

#include <emscripten/wasm_worker.h>
#include <stdio.h>

void run_in_worker()
{
  printf("Hello from Wasm Worker!\n");
}

int main()
{
  emscripten_wasm_worker_t worker = emscripten_malloc_wasm_worker(/*stackSize: */1024);
  emscripten_wasm_worker_post_function_v(worker, run_in_worker);
}

在編譯和連結步驟傳遞 Emscripten 旗標 -sWASM_WORKERS 來建置程式碼。範例程式碼會在主要瀏覽器執行緒上建立一個新的 Worker,該執行緒會共用相同的 WebAssembly.Module 和 WebAssembly.Memory 物件。然後將 postMessage() 傳遞給 Worker,要求它執行函式 run_in_worker() 以印出字串。

若要在建立 worker 時明確控制記憶體配置位置,請使用 emscripten_create_wasm_worker() 函式。此函式會接收一個記憶體區域,該區域必須夠大,才能容納 worker 的堆疊和 TLS 資料。您可以使用 __builtin_wasm_tls_size() 在執行階段找出程式的 TLS 資料需要多少空間。

簡介

在 WebAssembly 程式中,包含應用程式狀態的 Memory 物件可以在多個 Worker 之間共用。這可直接、高效能(如果未特別注意,則會產生競爭狀況!)同步在多個 Worker 之間共用資料狀態(共用狀態多執行緒)。

Emscripten 支援兩種多執行緒 API 來利用此網頁功能
  • POSIX 執行緒 (Pthreads) API 和

  • Wasm Workers API。

Pthreads API 在原生 C 程式設計和 POSIX 標準方面歷史悠久,而 Wasm Workers API 則是 Emscripten 編譯器獨有的。

這兩種 API 提供大致相同的功能集,但有重要的差異,本文件旨在說明這些差異,以協助決定應以哪個 API 為目標。

Pthreads 與 Wasm Workers:應該使用哪一個?

這兩種多執行緒 API 的目標對象和使用案例略有不同。

Pthreads API 的重點在於可攜性和跨平台相容性。此 API 最適合用於可攜性最重要的情况,例如,當程式碼庫被跨編譯到多個平台時,例如建置原生 Linux x64 可執行檔和以 Emscripten WebAssembly 為基礎的網站。

Emscripten 中的 Pthreads API 旨在仔細模擬原生 Pthreads 平台已經提供的相容性和功能。這有助於將大型 C/C++ 程式碼庫移植到 WebAssembly 上。

另一方面,Wasm Workers API 旨在提供與網頁上現有的網頁多執行緒基元的「直接對應」,並稱之為結束。如果應用程式僅開發用於以 WebAssembly 為目標,且不考慮可攜性,則使用 Wasm Workers 可以帶來很大的好處,例如簡化的編譯輸出、更低的複雜性、更小的程式碼大小,以及可能更好的效能。

但是,此好處可能不是明顯的勝利。Pthreads API 的設計旨在從同步 C/C++ 語言使用,而 Web Workers 的設計旨在從非同步 JavaScript 使用。WebAssembly C/C++ 程式可能會發現自己處於中間位置。

Pthreads 和 Wasm Workers 有幾個相似之處

  • 兩者都可以使用 emscripten_atomic_* Atomics API、

  • 兩者都可以使用 GCC __sync_* Atomics API、

  • 兩者都可以使用 C11 和 C++11 Atomics API、

  • 兩種執行緒都有本機堆疊。

  • 兩種執行緒都透過 thread_local (C++11)、 _Thread_local (C11) 和 __thread (GNU11) 關鍵字,支援執行緒本機儲存 (TLS)。

  • 兩種執行緒都透過明確連結的 Wasm 全域變數支援 TLS(例如,請參閱 test/wasm_worker/wasm_worker_tls_wasm_assembly.c/.S 的範例程式碼)

  • 兩種執行緒都有執行緒 ID 的概念(pthreads 的 pthread_self(),Wasm Workers 的 emscripten_wasm_worker_self_id()

  • 兩種執行緒都可以執行以事件為基礎和無限迴圈程式設計模型。

  • 兩者都可以使用 EM_ASMEM_JS API 在呼叫執行緒上執行 JS 程式碼。

  • 兩者都可以呼叫 JS 程式庫函式(使用 --js-library 指示詞連結),以在呼叫執行緒上執行 JS 程式碼。

  • pthreads 和 Wasm Workers 都不能與 -sSINGLE_FILE 連結器旗標一起使用。

但是,差異更為顯著。

Pthreads 可以代理 JS 函式

只有 pthreads 可以使用 MAIN_THREAD_EM_ASM*()MAIN_THREAD_ASYNC_EM_ASM() 函式,以及 JS 程式庫中的 foo__proxy: 'sync'/'async' 代理指示詞。

另一方面,Wasm Workers 不提供內建的 JS 函式代理工具。可以使用 emscripten_wasm_worker_post_function_* API 將該函式的位址明確傳遞給 Wasm Workers,來代理 JS 函式。

如果您需要從 Worker 中同步等待已張貼的函式完成,請使用其中一個 emscripten_wasm_worker_*() 執行緒同步函式來休眠呼叫執行緒,直到被呼叫者完成作業。

請注意,Wasm Workers 無法

Pthreads 有取消點

以效能和程式碼大小為代價,pthreads 實作了 POSIX 取消點 的概念 (pthread_cancel()pthread_testcancel())。

Wasm Workers 更輕量且效能更高,因為不啟用該概念。

Pthreads 可能會同步啟動 - Wasm Workers 一律會非同步啟動

建立新的 Worker 可能很慢。在 JavaScript 中產生 Worker 是非同步作業。為了支援同步 pthread 啟動(適用於需要它的應用程式)並提高執行緒啟動效能,pthreads 會託管在快取的 Emscripten 執行階段管理的 Worker 集區中。

Wasm Workers 省略此概念,因此 Wasm Workers 一律會非同步啟動。如果您需要偵測 Wasm Worker 何時已啟動,請在 Worker 與其建立者之間手動張貼 ping-pong 函式和回覆配對。如果您需要快速啟動新執行緒,請考慮自行管理 Wasm Worker 集區。

Pthread 拓撲是扁平的 - Wasm Workers 是階層式的

在網頁上,如果 Worker 產生自己的子 Worker,則會建立主要執行緒無法直接存取的巢狀 Worker 階層。為了規避此類拓撲所導致的可攜性問題,pthreads 會在幕後展平 Worker 建立鏈,以便只有主要瀏覽器執行緒才會產生執行緒。

Wasm Worker 不會實作這種拓樸扁平化,在 Wasm Worker 中建立 Wasm Worker 會產生巢狀的 Worker 階層。如果您需要在 Wasm Worker 內部建立 Wasm Worker,請考慮您想要的階層類型,如有必要,請自行將 Worker 建立操作透過訊息傳遞到主執行緒,手動扁平化階層。

請注意,各瀏覽器對巢狀 Worker 的支援程度不同。截至 2022 年 2 月,Safari 不支援巢狀 Worker。請參閱這裡以取得 polyfill。

Pthread 可以使用 Wasm Worker 的同步 API,反之則不行

如果需要,可以自由地從 pthread 內部調用 emscripten/wasm_worker.h 中提供的多執行緒同步基本元件(emscripten_lock_*emscripten_semaphore_*emscripten_condvar_*),但 Wasm Worker 無法使用 Pthread API 中的任何同步功能(pthread_mutex_*pthread_cond_pthread_rwlock_* 等),因為它們缺少所需的 pthread 執行階段環境。

Pthread 有「執行緒主」函式和 atexit 處理常式

pthread 的啟動/執行模型是啟動執行指定的執行緒進入點函式。當該函式退出時,pthread 也會(預設情況下)退出,而託管該 pthread 的 Worker 將會返回 Worker 池,等待在其上建立另一個執行緒。

相反地,Wasm Worker 實作直接的 Web 式模型,新建立的 Worker 會在其事件迴圈中閒置,等待函數傳遞給它。當這些函數完成時,Worker 將會返回其事件迴圈,等待接收更多要執行的函數(或 Worker 範圍的 Web 事件)。Wasm Worker 僅會在呼叫 emscripten_terminate_wasm_worker(worker_id)emscripten_terminate_all_wasm_workers() 時退出。

Pthread 允許使用者透過 pthread_atexit 註冊執行緒退出處理常式,這些處理常式將會在執行緒退出時呼叫。Wasm Worker 沒有這個概念。

Pthread 有每個執行緒的輸入 Proxy 訊息佇列,Wasm Worker 沒有

為了能夠在其他執行緒上彈性地同步執行程式碼,並實作對 MEMFS 檔案系統和螢幕外畫面緩衝區(從 Worker 模擬的 WebGL)等功能的支援 API,主瀏覽器執行緒和每個 pthread 都有一個系統支援的「Proxy 訊息佇列」來接收訊息。

這使得使用者程式碼可以從 emscripten/threading.h 呼叫 API 函式,例如 emscripten_sync_run_in_main_runtime_thread()emscripten_async_run_in_main_runtime_thread()emscripten_dispatch_to_thread() 等,來執行 Proxy 呼叫。

Wasm Worker 不提供此功能。如果需要,此類訊息傳遞應由使用者透過常規的多執行緒同步程式設計技術(互斥鎖、futex、號誌等)手動實作。

Pthread 同步時鐘時間

Pthread 提供的另一個輔助移植模擬功能是,emscripten_get_now() 返回的時間值會在所有執行緒之間同步到一個共同的時間基準。

Wasm Worker 省略了這個概念,建議在 Wasm Worker 中使用函式 emscripten_performance_now() 進行高效能計時,並避免跨 Worker 比較結果值,或手動同步它們。

輸入事件 API 僅反向 Proxy 到 pthread

emscripten/html5.h 中提供的多執行緒輸入 API 僅適用於 pthread API。當呼叫任何函式 emscripten_set_*_callback_on_thread() 時,可以選擇目標 pthread 作為接收事件的對象。

對於 Wasm Worker,如果需要,應手動實作從主瀏覽器執行緒到 Wasm Worker 的事件「反向 Proxy」,例如使用 emscripten_wasm_worker_post_function_*() API 系列。

但是請注意,反向 Proxy 輸入事件有一個缺點,它會阻止安全敏感的操作,例如全螢幕請求、指標鎖定和音訊播放恢復,因為處理輸入事件與執行初始操作的事件回呼內容是分開的。

pthread 與 emscripten_lock 實作差異

pthread_mutex_* 中的互斥鎖實作有幾種不同的建立選項,其中一種是「遞迴」互斥鎖。

emscripten_lock_* API 實作的鎖不是遞迴的(且不提供選項)。

Pthread 還提供程式設計保護,防止一個執行緒釋放另一個執行緒擁有的鎖這種程式設計錯誤。emscripten_lock_* API 不追蹤鎖的所有權。

記憶體需求

Pthread 對動態記憶體配置有固定的依賴性,並會呼叫 mallocfree 來配置執行緒特定的資料、堆疊和 TLS 插槽。

除了輔助函式 emscripten_malloc_wasm_worker() 之外,Wasm Worker 不依賴動態記憶體配置器。記憶體配置需求由呼叫端在 Worker 建立時滿足,並且如果需要,可以靜態放置。

產生程式碼大小

來自 pthread 的磁碟大小額外負擔約為數百 KB。另一方面,Wasm Worker 執行階段針對微型部署進行了最佳化,在磁碟上只有數百位元組。

API 差異

若要進一步了解 Pthread 和 Wasm Worker 之間可用的不同 API,請參閱下表。

功能 Pthread Wasm Worker
執行緒終止 執行緒呼叫
pthread_exit(status)
或主執行緒呼叫
pthread_kill(code)
Worker 無法自行終止,父執行緒透過呼叫終止
emscripten_terminate_wasm_worker(worker)
執行緒堆疊 在 pthread_attr_t 結構中指定。 使用以下方式明確管理執行緒堆疊區域
emscripten_create_wasm_worker_*_tls()
函式,或
使用以下方式自動配置堆疊 + TLS 區域
emscripten_malloc_wasm_worker()
API。
執行緒本機儲存 (TLS) 透明地支援。 透過以下方式明確支援
emscripten_create_wasm_worker_*_tls()
函式,或
透過以下方式自動支援
emscripten_malloc_wasm_worker()
API。
執行緒 ID 建立 pthread 會取得其 ID。呼叫
pthread_self()
以取得呼叫執行緒的 ID。
建立 Worker 會取得其 ID。呼叫
emscripten_wasm_worker_self_id()
取得呼叫執行緒的 ID。
高解析度計時器 ``emscripten_get_now()`` ``emscripten_performance_now()``
在主執行緒上的同步封鎖 同步基本元件內部會回退到忙碌旋轉迴圈。 明確的旋轉與睡眠同步基本元件。
Futex API
emscripten_futex_wait
emscripten_futex_wake
在 emscripten/threading.h 中
emscripten_atomic_wait_u32
emscripten_atomic_wait_u64
emscripten_atomic_notify
在 emscripten/atomic.h 中
非同步 futex 等待 不適用
emscripten_atomic_wait_async()
emscripten_*_async_acquire()
然而,這些是難以使用的陷阱,請閱讀 WebAssembly/threads issue #176
C/C++ 函式 Proxy emscripten/threading.h API 用於將函式呼叫 Proxy 到其他執行緒。 使用 emscripten_wasm_worker_post_function_*() API 將函式訊息傳遞到其他執行緒。這些訊息遵循事件佇列語義,而不是 Proxy 佇列語義。
建置旗標 使用 -pthread 編譯和連結 使用 -sWASM_WORKERS 編譯和連結
前處理器指令 __EMSCRIPTEN_SHARED_MEMORY__=1 和 __EMSCRIPTEN_PTHREADS__=1 處於活動狀態 __EMSCRIPTEN_SHARED_MEMORY__=1 和 __EMSCRIPTEN_WASM_WORKERS__=1 處於活動狀態
JS 程式庫指令 USE_PTHREADS 和 SHARED_MEMORY 處於活動狀態 USE_PTHREADS、SHARED_MEMORY 和 WASM_WORKER 處於活動狀態
Atomics API 支援,使用任何 __atomic_* API__sync_* APIC++11 std::atomic API
非遞迴互斥鎖
pthread_mutex_*
emscripten_lock_*
遞迴互斥鎖
pthread_mutex_*
不適用
號誌 不適用
emscripten_semaphore_*
條件變數
pthread_cond_*
emscripten_condvar_*
讀寫鎖
pthread_rwlock_*
不適用
自旋鎖
pthread_spin_*
emscripten_lock_busyspin*
WebGL 螢幕外畫面緩衝區
Supported with -sOFFSCREEN_FRAMEBUFFER
Not supported.

Wasm Worker 堆疊大小注意事項

在實例化 Wasm Worker 時,必須為建立的 Worker 建立 LLVM 資料堆疊的記憶體陣列。此資料堆疊通常只包含已被 LLVM「溢出」到記憶體中的局部變數,例如包含大型陣列、結構或其他透過記憶體位址參照的變數。此堆疊不會包含控制流程資訊。

由於 WebAssembly 不支援虛擬記憶體,因此為 Wasm Worker 和主執行緒定義的 LLVM 資料堆疊大小無法在執行階段成長。因此,如果 Worker(或主執行緒)用完堆疊空間,程式行為將會未定義。使用 Emscripten 連結器旗標 -sSTACK_OVERFLOW_CHECK=2,將執行階段堆疊溢位檢查發射到程式碼中,以便在開發期間偵測這些情況。

請注意,為了避免執行兩個單獨的配置,Wasm Worker 的 TLS 記憶體將會位於 Wasm Worker 堆疊空間的底端(低記憶體位址)。

Wasm Worker 與較早的 Emscripten Worker API

Emscripten 作為 emscripten.h 標頭的一部分,提供了第二個 Worker API。此 Worker API 早於 SharedArrayBuffer 的出現,與 Wasm Worker API 有很大的不同,這兩個 API 的命名相似只是由於歷史原因。

兩個 API 都允許從主執行緒產生 Web Worker,儘管語義不同。

使用 Worker API,使用者將可以從自訂 URL 產生 Web Worker。此 URL 可以指向一個完全獨立的 JS 檔案,該檔案不是使用 Emscripten 編譯的,以便從任意 URL 加載 Worker。使用 Wasm Worker 時,不會指定自訂 URL:Wasm Worker 將始終產生一個 Web Worker,該 Web Worker 在與主程式相同的 WebAssembly + JavaScript 環境中計算。

Worker API 不與 SharedArrayBuffer 整合,因此與載入的 Worker 的互動將永遠是非同步的。然而,Wasm Worker 是建立在 SharedArrayBuffer 之上,並且每個 Wasm Worker 共享主執行緒的相同 WebAssembly 記憶體位址空間並在其中計算。

Worker API 和 Wasm Worker API 都為使用者提供了向 Worker 發送 postMessage() 函式呼叫的能力。在 Worker API 中,此訊息傳送僅限於需要從主執行緒向 Worker 發起/啟動(在 <emscripten.h> 中使用 API emscripten_call_worker()emscripten_worker_respond())。然而,使用 Wasm Worker,使用者也可以將 postMessage() 函式呼叫傳送到其父(擁有)執行緒。

如果使用 Emscripten Worker API 發佈函式呼叫,目標 Worker URL 必須指向一個經過 Emscripten 編譯的程式(因此它必須具有 Module 結構,以便定位函式名稱)。只有已匯出到 Module 物件的函式才能被呼叫。使用 Wasm Workers 時,可以發佈任何 C/C++ 函式,而且不需要匯出。

在以下情況下使用 Emscripten Worker API:
  • 您想從未使用 Emscripten 建置的 JS 檔案中輕鬆產生 Worker。

  • 您想將一個單獨的已編譯程式產生為 Worker,而該程式與主執行緒程式不同,且主執行緒和 Worker 程式不共享通用程式碼。

  • 您不想要求使用 SharedArrayBuffer,或設定 COOP+COEP 標頭。

  • 您只需要使用 postMessage() 函式呼叫以非同步方式與 Worker 通訊。

在以下情況下使用 Wasm Workers API:
  • 您想在同一個 Wasm Module 環境中建立一個或多個同步計算的新執行緒。

  • 您想從同一個程式碼庫中產生多個 Worker,並透過在 Workers 之間共享 WebAssembly Module(目標程式碼)和記憶體(位址空間)來節省記憶體。

  • 您想透過使用原子操作和鎖定來同步協調執行緒之間的通訊。

  • 您的 Web 伺服器已配置所需的 COOP+COEP 標頭,以啟用網站上的 SharedArrayBuffer 功能。

限制

目前 Wasm Workers 不支援以下建置選項:

  • -sSINGLE_FILE

  • 動態連結 (-sLINKABLE, -sMAIN_MODULE, -sSIDE_MODULE)

  • -sPROXY_TO_WORKER

  • -sPROXY_TO_PTHREAD

範例程式碼

請參閱 test/wasm_workers/ 目錄,以取得不同 Wasm Workers API 功能的程式碼範例。