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 之間共用資料狀態(共用狀態多執行緒)。
POSIX 執行緒 (Pthreads) API 和
Wasm Workers API。
Pthreads API 在原生 C 程式設計和 POSIX 標準方面歷史悠久,而 Wasm Workers API 則是 Emscripten 編譯器獨有的。
這兩種 API 提供大致相同的功能集,但有重要的差異,本文件旨在說明這些差異,以協助決定應以哪個 API 為目標。
這兩種多執行緒 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_ASM
和EM_JS
API 在呼叫執行緒上執行 JS 程式碼。兩者都可以呼叫 JS 程式庫函式(使用
--js-library
指示詞連結),以在呼叫執行緒上執行 JS 程式碼。pthreads 和 Wasm Workers 都不能與
-sSINGLE_FILE
連結器旗標一起使用。
但是,差異更為顯著。
只有 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 實作了 POSIX 取消點 的概念 (pthread_cancel()
、pthread_testcancel()
)。
Wasm Workers 更輕量且效能更高,因為不啟用該概念。
建立新的 Worker 可能很慢。在 JavaScript 中產生 Worker 是非同步作業。為了支援同步 pthread 啟動(適用於需要它的應用程式)並提高執行緒啟動效能,pthreads 會託管在快取的 Emscripten 執行階段管理的 Worker 集區中。
Wasm Workers 省略此概念,因此 Wasm Workers 一律會非同步啟動。如果您需要偵測 Wasm Worker 何時已啟動,請在 Worker 與其建立者之間手動張貼 ping-pong 函式和回覆配對。如果您需要快速啟動新執行緒,請考慮自行管理 Wasm Worker 集區。
在網頁上,如果 Worker 產生自己的子 Worker,則會建立主要執行緒無法直接存取的巢狀 Worker 階層。為了規避此類拓撲所導致的可攜性問題,pthreads 會在幕後展平 Worker 建立鏈,以便只有主要瀏覽器執行緒才會產生執行緒。
Wasm Worker 不會實作這種拓樸扁平化,在 Wasm Worker 中建立 Wasm Worker 會產生巢狀的 Worker 階層。如果您需要在 Wasm Worker 內部建立 Wasm Worker,請考慮您想要的階層類型,如有必要,請自行將 Worker 建立操作透過訊息傳遞到主執行緒,手動扁平化階層。
請注意,各瀏覽器對巢狀 Worker 的支援程度不同。截至 2022 年 2 月,Safari 不支援巢狀 Worker。請參閱這裡以取得 polyfill。
如果需要,可以自由地從 pthread 內部調用 emscripten/wasm_worker.h
中提供的多執行緒同步基本元件(emscripten_lock_*
、emscripten_semaphore_*
、emscripten_condvar_*
),但 Wasm Worker 無法使用 Pthread API 中的任何同步功能(pthread_mutex_*
、pthread_cond_
、pthread_rwlock_*
等),因為它們缺少所需的 pthread 執行階段環境。
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 沒有這個概念。
為了能夠在其他執行緒上彈性地同步執行程式碼,並實作對 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 提供的另一個輔助移植模擬功能是,emscripten_get_now()
返回的時間值會在所有執行緒之間同步到一個共同的時間基準。
Wasm Worker 省略了這個概念,建議在 Wasm Worker 中使用函式 emscripten_performance_now()
進行高效能計時,並避免跨 Worker 比較結果值,或手動同步它們。
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_mutex_*
中的互斥鎖實作有幾種不同的建立選項,其中一種是「遞迴」互斥鎖。
emscripten_lock_*
API 實作的鎖不是遞迴的(且不提供選項)。
Pthread 還提供程式設計保護,防止一個執行緒釋放另一個執行緒擁有的鎖這種程式設計錯誤。emscripten_lock_*
API 不追蹤鎖的所有權。
Pthread 對動態記憶體配置有固定的依賴性,並會呼叫 malloc
和 free
來配置執行緒特定的資料、堆疊和 TLS 插槽。
除了輔助函式 emscripten_malloc_wasm_worker()
之外,Wasm Worker 不依賴動態記憶體配置器。記憶體配置需求由呼叫端在 Worker 建立時滿足,並且如果需要,可以靜態放置。
來自 pthread 的磁碟大小額外負擔約為數百 KB。另一方面,Wasm Worker 執行階段針對微型部署進行了最佳化,在磁碟上只有數百位元組。
若要進一步了解 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_* API 或 C++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 時,必須為建立的 Worker 建立 LLVM 資料堆疊的記憶體陣列。此資料堆疊通常只包含已被 LLVM「溢出」到記憶體中的局部變數,例如包含大型陣列、結構或其他透過記憶體位址參照的變數。此堆疊不會包含控制流程資訊。
由於 WebAssembly 不支援虛擬記憶體,因此為 Wasm Worker 和主執行緒定義的 LLVM 資料堆疊大小無法在執行階段成長。因此,如果 Worker(或主執行緒)用完堆疊空間,程式行為將會未定義。使用 Emscripten 連結器旗標 -sSTACK_OVERFLOW_CHECK=2,將執行階段堆疊溢位檢查發射到程式碼中,以便在開發期間偵測這些情況。
請注意,為了避免執行兩個單獨的配置,Wasm Worker 的 TLS 記憶體將會位於 Wasm Worker 堆疊空間的底端(低記憶體位址)。
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 建置的 JS 檔案中輕鬆產生 Worker。
您想將一個單獨的已編譯程式產生為 Worker,而該程式與主執行緒程式不同,且主執行緒和 Worker 程式不共享通用程式碼。
您不想要求使用 SharedArrayBuffer,或設定 COOP+COEP 標頭。
您只需要使用 postMessage() 函式呼叫以非同步方式與 Worker 通訊。
您想在同一個 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 功能的程式碼範例。