注意
已實作並啟用 SharedArrayBuffer 的瀏覽器,正在將其置於跨來源開啟器政策 (COOP) 和跨來源嵌入器政策 (COEP) 標頭之後。除非正確設定這些標頭,否則 Pthreads 程式碼將無法在已部署的環境中運作。如需詳細資訊,請按一下此連結
Emscripten 支援在瀏覽器中使用 SharedArrayBuffer 進行多執行緒處理。該 API 允許在主執行緒和 Web Worker 之間共享記憶體,以及用於同步的原子操作,這使 Emscripten 能夠實作對 Pthreads (POSIX 執行緒) API 的支援。此支援在 Emscripten 中被視為穩定。
依預設,不啟用 Pthreads 的支援。若要啟用 Pthreads 的程式碼產生,存在下列命令列旗標
編譯任何 .c/.cpp 檔案時,以及連結以產生最終輸出 .js 檔案時,請傳遞編譯器旗標 -pthread
。
或者,傳遞連結器旗標 -sPTHREAD_POOL_SIZE=<expression>
以指定在應用程式 main()
呼叫之前,於頁面 preRun
時間填入的預定義 Web Worker 集區。這很重要,因為如果 Worker 尚未存在,我們可能需要等待下一個瀏覽器事件反覆運算才能執行某些操作,請參閱下文。<expression>
可以是任何有效的 JavaScript 運算式,包括整數 (如 8
表示固定數量的執行緒),或例如 navigator.hardwareConcurrency
表示建立與 CPU 核心數相同的執行緒。
應該不需要其他變更。在 C/C++ 程式碼中,可以使用前置處理器檢查 #ifdef __EMSCRIPTEN_PTHREADS__
來偵測 Emscripten 目前是否以 Pthreads 為目標。
注意
不可能建置一個在可用時能夠利用多執行緒,而在不可用時回退到單執行緒的二進位檔。您可以執行的最佳操作是兩個不同的建置,一個有執行緒,一個沒有執行緒,並在執行時間選擇其中一個。
-sPROXY_TO_PTHREAD
:在此模式下,您原始的 main()
會被建立 Pthread 並且在其上執行原始 main()
的新 main()
取代。因此,您的應用程式的 main()
會在瀏覽器主 (UI) 執行緒之外執行,這有助於提高回應能力。當有事物被代理到瀏覽器主執行緒時,瀏覽器主執行緒仍然會執行程式碼,例如處理事件、轉譯等。主執行緒也會執行諸如為您建立 Pthread 之類的操作,以便您可以同步依賴它們。
請注意,Emscripten 有 --proxy-to-worker
連結器旗標,聽起來很相似,但彼此無關。該旗標不使用 Pthreads 或 SharedArrayBuffer,而是使用純 Web Worker 來執行您的主程式 (並使用 postMessage 來回代理訊息)。
Web 允許某些操作僅在主瀏覽器執行緒中發生,例如與 DOM 互動。因此,如果在背景執行緒上呼叫各種操作,則會將它們代理到主瀏覽器執行緒。如需更多資訊,請參閱錯誤 3495,以及如何嘗試解決此問題。若要檢查哪些操作被代理,您可以查看 JS 程式庫 (src/library_*
) 中的函式實作,並查看它是否以 __proxy: 'sync'
或 __proxy: 'async'
註解;但是,請注意,瀏覽器本身會代理某些操作 (例如某些 GL 操作),因此沒有通用的方法可以確保安全 (除了不在主瀏覽器執行緒上封鎖之外)。
此外,Emscripten 目前有一個簡單的檔案 I/O 模型,該模型僅在主應用程式執行緒上發生 (因為我們支援無法共享記憶體的 JS 外掛程式檔案系統);這是另一組被代理的操作。
在某些情況下,代理可能會導致問題,請參閱下面的封鎖章節。
請注意,在大多數情況下,「主瀏覽器執行緒」與「主應用程式執行緒」相同。主瀏覽器執行緒是網頁開始執行 JavaScript 的位置,也是 JavaScript 可以存取 DOM 的位置 (網頁也可以建立 Web Worker,該 Web Worker 將不再位於主執行緒上)。主應用程式執行緒是您啟動應用程式的執行緒 (透過載入 Emscripten 發出的主 JS 檔案)。如果您在主瀏覽器執行緒上啟動它 (透過它是普通的 HTML 頁面),則兩者是相同的。但是,您也可以在 Worker 中啟動多執行緒應用程式;在這種情況下,主應用程式執行緒是該 Worker,並且無法存取主瀏覽器執行緒。
原子操作的 Web API 不允許在主執行緒上阻塞(明確地說,Atomics.wait
在這裡不起作用)。諸如 pthread_join
之類的 API,以及任何在底層使用 futex 等待的 API(例如 usleep()
、emscripten_futex_wait()
或 pthread_mutex_lock()
)都需要這種阻塞。為了讓它們正常運作,我們在瀏覽器主執行緒上使用忙碌等待,這會導致瀏覽器分頁沒有回應,也會浪費電力。(在 pthread 上,這不是問題,因為它在 Web Worker 中執行,我們不需要忙碌等待。)
儘管有上述缺點,在瀏覽器主執行緒上忙碌等待通常還是可以運作的,例如等待輕度競爭的互斥鎖。但是,諸如 pthread_join
和 pthread_cond_wait
之類的操作通常旨在阻塞很長一段時間,如果這種情況發生在瀏覽器主執行緒上,而且其他執行緒期望它做出回應,則可能會導致令人驚訝的死鎖。這可能是因為代理造成的,請參閱上一節。如果主執行緒在工作執行緒嘗試代理到它時阻塞,就可能會發生死鎖。
重點是,在 Web 上,讓瀏覽器主執行緒等待任何其他操作是不好的。因此,預設情況下,如果 pthread_join
和 pthread_cond_wait
發生在瀏覽器主執行緒上,Emscripten 會發出警告,如果 ALLOW_BLOCKING_ON_MAIN_THREAD
為零,則會拋出錯誤(其訊息將指向這裡)。
為避免這些問題,您可以使用 PROXY_TO_PTHREAD
,如前所述,它會將您的 main()
函式移至 pthread,讓瀏覽器主執行緒只專注於接收代理事件。一般來說,這是推薦的做法,但如果應用程式假設 main()
在瀏覽器主執行緒上,則可能需要進行一些移植工作。
另一種選擇是用非阻塞呼叫取代阻塞呼叫。例如,您可以用 pthread_tryjoin_np
取代 pthread_join
。這可能需要您重構應用程式以使用非同步事件,可能透過 emscripten_set_main_loop()
或 ASYNCIFY。
Emscripten 的 pthreads API 實作應該嚴格遵循 POSIX 標準,但仍然存在一些行為差異
當呼叫 pthread_create()
時,如果我們需要建立新的 Web Worker,則需要返回主事件迴圈。也就是說,您不能呼叫 pthread_create
,然後同步執行期望工作執行緒開始執行的程式碼 - 它只會在您返回事件迴圈後執行。這違反了 POSIX 行為,並且會破壞建立執行緒並立即加入它,或以其他方式同步等待觀察效果(例如記憶體寫入)的常見程式碼。對此有多種解決方案
返回主事件迴圈(例如,使用 emscripten_set_main_loop
或 Asyncify)。
使用連結器標誌 -sPTHREAD_POOL_SIZE=<expression>
。使用池會在呼叫 main 之前建立 Web Worker,因此在呼叫 pthread_create
時可以直接使用它們。
使用連結器標誌 -sPROXY_TO_PTHREAD
,它會為您在工作執行緒上執行 main()
。這樣做時,pthread_create
會被代理到瀏覽器主執行緒,以便在需要時返回主事件迴圈。
Emscripten 實作不支援 POSIX 信號,有時會與 pthreads 結合使用。這是因為不可能向 Web Worker 發送信號並搶佔它們的執行。唯一的例外是 pthread_kill(),它可以像往常一樣使用來強制終止正在執行的執行緒。
Emscripten 實作也不支援透過 fork()
和 join()
進行多處理。
出於 Web 安全目的,在 Firefox Nightly 中執行時,可以產生的執行緒數量有一個固定限制(預設為 20)。#1052398。要調整限制,請導覽至 about:config 並變更 pref「dom.workers.maxPerDomain」的值。
pthreads 規格中的某些功能不受支援,因為 Emscripten 使用的上游 musl 函式庫不支援它們,或者它們被標記為可選,符合規範的實作不需要支援它們。Emscripten 中不受支援的功能包括執行緒的優先順序,以及 pthread_rwlock_unlock() 不會按照執行緒優先順序執行。函式 pthread_mutexattr_set/getprotocol()、pthread_mutexattr_set/getprioceiling() 和 pthread_attr_set/getscope() 是空操作。
在移植時需要特別注意的一點是,有時在現有程式碼庫中,pthread_create() 和 pthread_cleanup_push() 的回呼函式指標會省略 void* 引數,這嚴格來說在 C/C++ 中是未定義的行為,但在幾個 x86 呼叫慣例中可以運作。在 Emscripten 中執行此操作會發出編譯器警告,並且在嘗試使用不正確的簽名呼叫函式指標時,可能會在執行階段中止,因此在存在此類錯誤的情況下,最好檢查執行緒回呼函式的簽名。
請注意,函式 emscripten_num_logical_cores() 將始終傳回 navigator.hardwareConcurrency 的值,也就是系統上的邏輯核心數量,即使不支援共用記憶體也是如此。這表示 emscripten_num_logical_cores() 可能會傳回大於 1 的值,同時 emscripten_has_threading_support() 可以傳回 false。emscripten_has_threading_support() 的傳回值表示瀏覽器是否具有可用的共用記憶體支援。
Pthreads + 記憶體成長 (ALLOW_MEMORY_GROWTH
) 特別棘手,請參閱 Wasm 設計問題 #1271。這目前會導致 JS 存取 Wasm 記憶體的速度變慢 - 但這可能只有在 JS 執行大量記憶體讀寫時才會明顯(Wasm 以全速執行,因此移轉工作可以解決此問題)。這也需要您的 JS 知道 HEAP* 視圖可能需要更新 - 使用 --js-library
等嵌入的 JS 程式碼會自動轉換為在使用 HEAP*
的地方使用 GROWABLE_HEAP_*
協助程式函式,但直接使用 Module.HEAP*
的外部程式碼可能會遇到視圖小於記憶體的問題。
Emscripten 中的預設系統配置器 dlmalloc
在單執行緒程式中非常有效率,但它有一個單一的全域鎖,這表示如果 malloc
上有競爭,您可能會看到額外負荷。您可以改用 mimalloc,方法是使用 -sMALLOC=mimalloc
,這是一種更複雜的配置器,專為多執行緒效能而調整。mimalloc
在每個執行緒上都有個別的配置內容,允許在 malloc/free
競爭下效能更好地擴展。
請注意,mimalloc
的程式碼大小比 dlmalloc
大,而且在執行階段也會使用更多記憶體(因此您可能需要將 INITIAL_MEMORY
調整為更高的值),因此這裡有一些取捨。
任何啟用 pthreads 支援編譯的程式碼目前只能在 Firefox Nightly 通道中運作,因為 SharedArrayBuffer 規格在標準化之前仍處於實驗性研究階段。有兩個測試套件可用於驗證 Emscripten 中 pthreads API 實作的行為
Emscripten 單元測試套件在「browser.」套件中包含幾個特定於 pthreads 的測試。執行任何名稱為 browser.test_pthread_* 的測試。
在 juj/posixtestsuite GitHub 儲存庫中提供 Emscripten 專門版本的 Open POSIX Test Suite。此套件包含約 300 個用於 pthreads 一致性的測試。若要執行此套件,應先將 pref dom.workers.maxPerDomain 增加到至少 50。
如果發生任何問題,請先檢查這些。錯誤可以像往常一樣報告給 Emscripten 錯誤追蹤器。