多執行緒 Rust 和 Wasm
WebAssembly 首次發布時,它是一個MVP,雖然很簡陋,但已產生大量令人興奮的專案,可以在所有主流瀏覽器上運作。Rust 也充分利用了 wasm MVP 的成功,並開發了wasm-bindgen
和wasm-pack
等工具,讓 MVP 顯得不再那麼簡陋。不過,WebAssembly「更具野心」!自開發以來,它一直打算以新功能擴充WebAssembly 規範。
其中一個我特別期待 WebAssembly 逐步開發的功能,就是執行緒提案。執行緒提案不幸地在Spectre 和 Meltdown漏洞首次出現後被迫暫停,但現在又開始快速發展了!各瀏覽器很快就會開始發布SharedArrayBuffer
,而 wasm 的執行緒也很快就會隨之而來。
wasm 的執行緒等功能,會對 Rust 及其在網路上的使用方式造成重大影響,我們希望確定一旦執行緒可用,Rust 就已經準備好且非常適合使用 wasm 執行緒!我最近開始嘗試更多參與WebAssembly 社群小組,這似乎是測試 Rust 支援度的絕佳機會,同時如果需要,我們還期望能為提案本身提供意見回饋!
如果你迫不及待想看結局,可以跳到結尾(有雷)有一個範例展示了 Rust、WebAssembly 和執行緒,全部都在瀏覽器中。
注意:要讓未來的讀者留心,這篇文章說明和使用許多在撰寫時還不穩定的功能。這裡提到的內容可能不是未來都正確,而範例也可能無法再執行。我們會盡量保持內容最新,但如果你閱讀這篇文章的時間和撰寫時間相隔已久,請先保持懷疑的態度!
WebAssembly 執行緒提案
儘管家可能會天真地認為「WebAssembly 執行緒」的概念類似於「新增 pthreads」或「新增 std::thread
」給 wasm,對於 WebAssembly 中執行緒的現行提案實際上完全不同!此執行緒提案並未提供完整的函式庫體驗,而是針對您可以建立執行緒函式庫的基本建構模組進行規格說明。
原子指令
您可能會注意到的執行緒提案中的第一個面向是加入了 原子指令。用 Rust 的用語來說,這表示 AtomicUsize
和相關項目實際上會編譯成原子操作,而它們目前則是 簡化成單執行緒等效項(因為沒有執行緒!)。這些指令儘管必要,但是只到您使用 wait
和 notify
時才算有趣。
原子修改允許我們執行一定程度的同步作用,但完整的同步作用通常需要實際封鎖一個執行緒,直到另一個執行緒完成為止。這就是 i32.atomic.wait
和 atomic.notify
指令發揮作用的地方。首先,我們可以使用 i32.atomic.wait
(以原子方式)封鎖一個執行緒,然後另一個執行緒可以執行 atomic.notify
來喚醒一個封鎖在同一地址上的執行緒。我相信這類似於 Linux 上的 futex,儘管我本人從未使用過!
只要加入這一個新增項目,我們現在就可以開始了解原始類型如何形成,而此提案也肯定有一個 範例互斥鎖實作,Rust 的 Mutex
類型 也是這樣實作的。
好吧,這些很棒,但是我們要如何產生多過一個執行緒?
透過 Web Workers 進行平行化
WebAssembly 最大的優勢之一是它會延伸網路平台,而非試圖取代它。儘管 wasm 模組本身基本上只能直接操作數字,但是它們可以匯入任何任意函式,讓 wasm 能夠完整存取網路平台、DOM 和所有內容。從第一天開始,WebAssembly 就專注於重新使用和強化網路平台體驗,避免因為新功能而必須重新發明輪子。
這項提議並不例外!網路已經透過 Web Workers 來支援多執行緒的網路應用程式,而這正是打算用來替 WebAssembly 介紹多執行緒執行的部分內容。
不過,Web Workers 提供的能力相當有限,只能在執行緒間分享資源。傳遞訊息和同步是透過訊息傳遞 (postMessage
) 來完成的,但您只能傳送支援 結構複製 的值。以 Rust-ic 術語來說,JS 中有少數類型是 Send
,而傳送物件至另一個執行緒時,您會一直執行 Clone
。
不過,我們的目標是要分享資源!事實證明,支援結構複製的其中一種類型是 WebAssembly.Module
。目前在網路上執行 wasm 需要使用 wasm JS API,您會建立一個類似於可執行的純文字和資料區段 (已編譯程式碼) 的 WebAssembly.Module
,接著從該模組建立一個 WebAssembly.Instance
,這才是您實際取得像是堆疊和儲存空間等項目的地方。我們現在已經可以讓不同的執行緒傳遞 WebAssembly.Module
,儘管必須讓執行緒之間具備 Clone
能力,但看起來在大部分的引擎中大致如下所示
pub struct Module {
contents: Arc<ModuleContents>,
}
// ...
這表示複製作業相當便宜!
不過,分享我們的程式碼只是其中一半。許多語言 (包括 Rust) 也依賴共享記憶體作為基礎,才能建構訊息傳遞或變數鎖定等各種並行處理典範。
共享記憶體
延續「不只是為了 wasm 而帶入基本的全新功能」這個主題,共享記憶體主要是建立在一個穩定的 (或是,即將變得穩定) JS API SharedArrayBuffer
上。 SharedArrayBuffer
類似於 ArrayBuffer
,但它是共享的!這是透過結構複製演算法來體現的,其中您可以將它想像成類似 WebAssembly.Module
,內部包含一個容易複製的 Arc
。
使用 SharedArrayBuffer
,JS 已經可以讓執行緒與主執行緒共享記憶體,這讓計算大塊資料變得便宜,同時將其傳送至其他執行緒。(或者至少能省去在執行緒之間複製資料的必要。)
現今的 WebAssembly 模組最多可備選關聯到一個 “線性記憶體” 執行個體。若以非 wasm 術語來說的話,你可以將一支 RAM 棒插入 wasm 模組。這個 WebAssembly.Memory
現今都永遠由一個 ArrayBuffer
做後盾,但你很快就能將記憶體標記為「共用」,這表示它將由 SharedArrayBuffer
做後盾。這進而表示由 SharedArrayBuffer
做後盾的 WebAssembly.Memory
結構化複製將會參考相同的記憶體!
在這個階段,各個部分絕對已經開始整合在一起。我們已經可以在各個執行緒之間共用模組(程式碼),再過不久我們也能在各個執行緒之間共用記憶體!有了這些新功能,我們可以快速有效地在多個網頁工作執行緒上建立 WebAssembly.Module
,而這些執行緒都能存取相同的記憶體。
一次初始化記憶體
WebAssembly 模組的一個有趣方面,在於記憶體會自動為你初始化。舉例來說,如果你有一個 Rust 程式,看起來像
#[no_mangle]
pub extern fn get_data() -> *const u8 {
"the data".as_ptr()
}
如果我們從 JS 呼叫它,而且我們讀取回傳的指標,我們實際上會看到 資料
!不過,到底是誰將那些位元組寫入到線性記憶體的?每個 wasm 模組都能有 資料區段,指定一塊位元組,位於記憶體中的一個位移量。在建立模組時,wasm 執行階段會將每個這些資料區段複製到指定位移量,放入線性記憶體。
但是等等,如果我們在多個執行緒上建立我們的模組,這並不是個好主意!舉例來說,我們的程式碼可能看起來像
#[no_mangle]
pub extern fn get_ticket() -> usize {
static TICKET: AtomicUsize = AtomicUsize::new(1);
TICKET.fetch_add(1, SeqCst)
}
在這裡我們將有一個資料區段,包含 1usize
,這會是三個零位元組,接著是一個一位元組。每當我們建立我們的模組,我們會將這個計數器重設為 1,方法是覆寫上一個值!我們想要的做法是,第一個執行緒初始化記憶體,而所有其他執行緒僅使用已經存在在那裡的記憶體。
為了解決此問題,我們求助於 大量記憶體操作提案。雖然大量記憶體操作提案主要始於 memcpy
和 memset
的原生方法,但是現在它也具備「被動式記憶體區段」的能力,可以準確解決我們遇到的問題。
每個資料區段都可以標示為「被動式」,這表示在建立執行個體時並不會自動將其複製到記憶體。模組必須透過 memory.init
指令手動初始化記憶體。使用 memory.init
可以將記憶體從任何資料區段複製到記憶體的任何位置。
使用 memory.init
至少能讓我們解決多個初始化的問題,但是我們要如何透過工具鏈利用它還不明確。之後會有更多說明!
現有的 WebAssembly 功能和執行緒
這大概涵蓋了 WebAssembly 執行緒(和大量記憶體)提案中建議的新功能。但是,在我們深入探討如何實際使用此功能之前,值得快速瀏覽一下 WebAssembly 現有功能,以及它們在使用執行緒的情況下代表什麼意義。
第一個有趣的方面(我們會在稍後利用)是 start
函式。WebAssembly 模組可以標記一個函式,使其在建立執行個體時自動執行。此 start
函式可以處理一些事項,例如靜態初始化,甚至可能處理程式碼模擬為執行檔的 main
函式,但目前並沒有在 Rust 中揭露或使用。執行緒提案中 start
函式的語意完全沒有改變,但是這表示它不再是一次性初始化!start
函式仍每個執行個體執行一次,由於我們在多個網路工作執行緒上建立多個執行個體,start
函式變得多像「執行緒初始化」而非「全域初始化」。之後會有更多說明!
接下來讓我們看 global
變數。注意,這些不是 Rust static
變數(例如上述的 TICKET
),因為後者是在編譯時放置在線性記憶體中。事實上,Rust 目前並未提供建立、取得或設定自訂 global
變數的能力,因此這在 Rust 中普遍來說是未揭露的 WebAssembly 功能。不過在 WebAssembly 中,global
正如其名,是執行個體的變數,可以取得、設定,甚至可以匯出到 JS!全域變數更像是虛擬暫存器而非線性記憶體,因為它只能包含一系列固定類型。
不過,有趣的是,全域變數是與實例相關的。這表示在多實例的世界中,它們實際上不是全域變數,而是執行緒內部變數!每個「WebAssembly」實例都有自己的一組全域變數,不能由其他實例存取,為我們提供執行緒內部資料的基礎。稍後還會詳細說明!
在具備執行緒的「WebAssembly」世界中,表格也可能有一些有趣的用例,但我自己不確定它們是什麼,所以目前我們將大致忽略它們。除此之外,這應該涵蓋了大部分「WebAssembly」功能以及它們與執行緒的關聯!
在 Rust 中使用執行緒
現在我們已經討論過「WebAssembly」執行緒提案的重點,你可能會和我第一次讀到它時的心情一樣。當然,所有這些功能聽起來都不錯,但在語言層面如何安全、符合人體工學地公開這些功能?有些問題很簡潔明瞭,例如 Rust 中 Mutex
的實作,但還有許多其他問題無法簡潔明瞭地自我包含,例如
-
首先,堆疊!Rust 的程式碼產生器 LLVM 假設它不僅能使用原生「WebAssembly」堆疊(它是與實例相關的,因此是「執行緒內部的」),而且還能使用線性記憶體堆疊。這表示我們需要對線性記憶體有一支堆疊指標(LLVM 已將其方便地放置在
global
中),且每一個執行緒都是唯一的,而且有人必須為每個執行緒分配這些堆疊。 -
接下來,執行緒內部資料。我們有
global
變數這個執行緒內部資料的基礎,但如前所述,Rust(和 LLVM 或 LLD)實際上並未提供操作或使用自訂global
變數的能力。我們要如何實作 Rust 中標準函式庫的thread_local!
巨集? -
我們稍早討論了一點記憶體初始化,以及我們不想要重新初始化並抹掉記憶體,但實際上誰會這麼做?想必我們所有的資料區段都需要是
passive
,但誰會安全地執行memory.init
? -
我們實際上要如何建立執行緒?創建網頁工作者實際是由誰負責?同樣地,
WebAssembly.Module
和WebAssembly.Memory
是透過什麼機制在工作者之間傳輸的,並在正確的地方實例化? -
在使用
wasm-bindgen
等工具時,shim JS 是如何傳遞給所有擁有「WebAssembly」實例的工作者的?這種包裝器 JS 是讓呼叫 Rust 更符合人體工學所必需的,而且我們不想給主執行緒過多好處!
很遺憾,今天我們無法回答所有這些問題。當我們不希望它們出現時,這些問題也會彼此糾纏在一起!
Rust 對 WebAssembly on the Web 的願景之一就是可互操作性。你應該可以使用 Rust 和 WebAssembly,而你的應用程式不需知道這件事。而且,位於你的依賴圖深處的板條箱可能會依賴 JS 功能 (例如 NPM 套件或 web-sys
),而且你也不應該知道這個問題!
目前尚不清楚我們是否能維持這個 Web 平臺上執行緒的願景。這正是我希望與大家集思廣益、並獲得相關協助和想法之處。畢竟,執行緒提案不穩定,理論上我們還有很多時間想出辦法來解決這個問題!
不過,我不想讓你失望!儘管上述問題目前沒有完美的答案,但我一直在努力找出至少一個可用的方法,來解決特別是 wasm-bindgen
中的許多問題。讓我們來探討這個方法,看看今天是否能實際展示執行緒和 WebAssembly!
執行緒和 wasm-bindgen
wasm-bindgen
工具包含兩半。一半是程序巨集,也就是 #[wasm_bindgen]
屬性,這會在編譯時擴充並執行。這會在你的 Rust 程式碼中產生 shim,並準備好給第二半的最終二進位檔案,也就是 wasm-bindgen
CLI。CLI 工具 wasm-bindgen
獨具優勢,能夠對 WebAssembly 模組進行各種驚人的轉換 (而且它已經做到了)!
WebAssembly 二進位檔案格式已完整說明,而且令人驚訝的是易於操作。目前 wasm-bindgen
CLI 工具使用優秀的 parity-wasm
板條箱來分析解析 WebAssembly,這讓 wasm-bindgen
可以輕易進行華麗的轉換。(很快會有更多相關消息,一個更輕易的解決方案也正在進行中!)
透過 CLI 工具和 parity-wasm
,我們擺脫了 LLVM 的「枷鎖」(也就是說,在工具中進行實驗較在 LLVM 本身中進行更容易),而且可以存取 WebAssembly 的全功能組。讓我們用這個新發現的力量解決上面的一些問題。
注入執行緒區域變數 global
儘管 LLVM/LLD 目前沒有能力發出自訂的 global
變數,但在 wasm-bindgen
中可以做到!這是一個實作執行緒區域儲存空間的簡易方法,因此讓我們讓 wasm-bindgen
注入兩個 global
-
首先,一個執行緒識別碼。執行緒識別碼對許多應用程式來說很有用,但我們現在特別有興趣探討標準函式庫中的
ReentrantMutex
,它需要知道哪個執行緒是哪個,才能知道何時使用可重入鎖。 -
接著,TCB 插槽。TCB 是「執行緒控制區塊」,通常用於儲存執行緒執行階段中已配置的結構。此已配置結構是許多其他執行階段相關功能的進入點,但目前我們會將其主要用於儲存使用者定義的執行緒局部值。換句話說,我們將藉此實作
thread_local!
。
在 Wasm 模組中新增兩個 global
型別的 i32
變數相當容易,但我們也需要管理它們!畢竟還是得有人來配置執行緒識別碼,而且我們也需要能夠存取它。
針對這點,可以採用 wasm-bindgen
策略的另一項絕招,重寫函式呼叫。我們定義一個以這種方式引入的函式
#[link(wasm_import_module = "__wbindgen_thread_xform__")]
extern {
fn __wbindgen_thread_id() -> u32;
}
實際上透過神奇方式轉換為 get_global $thread_id
。call
指令實際上有個 1:1 替換,就是 get_global
,因此這裡的重寫非常簡單!我們也可以使用類似 __wbindgen_tcb_get
和 __wbindgen_tcb_set
等「內置函數」來取得/設定 TCB。
接著,我們來思考如何初始化此執行緒識別碼共用 global 變數。
要從哪裡 開始
?
我們前面看到 WebAssembly 提供了一個 start
函式,每當實例化模組時會自動呼叫此函式,而在多執行緒環境中,這屬於每個執行緒的初始化。這恰好正符合我們的需求,包括執行緒識別碼初始化以及其他面向!
我們可以使用 wasm-bindgen
解決上述許多問題,透過插入的 start
函式。我們甚至可以在完成時呼叫之前的 start
函式,以維持語意等價!我們的插入函式將執行下列步驟
-
原子化遞增插入的共用執行緒識別碼計數器。我們在
wasm-bindgen
中保留 4 位元組的線性記憶體空間,此位址將追蹤所有執行緒曾經存在的狀態。然後可以將此原子化加總的結果儲存在我們的執行緒識別碼共用 global 變數中,表示我們剛剛配置並初始化了執行緒識別碼! -
如果我們的執行緒識別碼為零,表示我們是第一個執行緒(主執行緒)。此時非常適合初始化記憶體,因此
wasm-bindgen
可以將我們所有資料區段標示為passive
,如果我們的識別碼為 0,我們可以呼叫memory.init
。 -
如果我們的執行緒 ID 不是 0,代表我們是一個衍生執行緒。LLVM 已經安排了一個做為我們堆疊指標的
全域
變數,但它的初始值僅對主執行緒有效。為了繼續執行,我們需要設定這個變數。為了配置堆疊,我們可以利用便捷的memory.grow
指令,這是一種在不使用標準函式庫中實際記憶體配置器(呼叫該指令又需要一個堆疊)的情況下配置記憶體的快速且骯髒的方法。當我們取得堆疊後,就可以更新堆疊指標的全球變數了! -
最後,如果先前的
start
函式可用,我們可以在這個時間點委派給函式並呼叫函式。
好的,我們的進度良好!透過假設所有執行緒使用完全相同的 WebAssembly.Module
,注入的 start
函式可以做很多事情來建立執行緒,而且很容易使用。
管理 WebAssembly.Memory
預設情況下,所有 Rust 編譯的 WASM 二進位檔都會匯出它們定義的 memory
。這表示 WASM 模組的執行個體會自動建立一個 WebAssembly.Memory
個體,並讓它可以供使用。然而,這與執行緒不相容,因為我們希望所有模組都使用相同的記憶體個體!
反之,我們需要安排匯入記憶體,而不是匯出記憶體。儘管這在設定和使用方面會有些麻煩,不過 wasm-bindgen
可以繼續處理 JS 繫結中的實例化,讓使用者不必擔心這個問題。
請注意,目前 LLVM/LLD 也未執行標示為 shared
的記憶體物件,因此 wasm-bindgen
也可以處理這個小細節。
分享 JS shim,衍生工作人員
就是這個時候,與 wasm-bindgen
相關的故事開始有點「這個特殊策略不再看起來長期可行」的味道。我們最後需要處理的事情實際上是衍生網路工作人員,並設法讓 WebAssembly.Module
和 WebAssembly.Memory
進入每個工作人員。
我最初嘗試透過 Webpack 來達成此目的,因為對大型整合來說,擁有一個套件管理員會很棒。但不幸地我遇到了幾個問題,例如 無法存取 WebAssembly.Module
,另外我不太清楚工作執行緒要如何使用不同的實例化路徑,才能用 onmessage
等待模組/記憶體,然後在收到後實例化。不過不用擔心,我確定我們無論如何都能找出一個適合的套件管理員!
接下來我轉而使用 wasm-bindgen
的 --no-modules
選項,看看是否可行。目前它會匯出一項名為 wasm_bindgen
的全域變數,此函式會接收要實例化的 wasm 檔案路徑。我將它擴充為接受 WebAssembly.Module
實例 (連同 WebAssembly.Memory
)。這樣一來,當提供路徑時,它可以建立記憶體並執行擷取/實例化,但透過 WebAssembly.Module
,它可以避免擷取並使用所提供的記憶體來實例化。
由於 --no-modules
使得設定所有事項變得相當手動,因此只要讓主執行緒照常工作,提供模組/記憶體的存取器、啟動 Web 工作執行緒,並將模組/記憶體張貼到每個工作執行緒即可。在工作執行緒內,我們可以匯入 --no-modules
所產生的 JS,等待訊息,等待實例化,然後開始執行一些工作。
總而言之,此設定使某項任務 得以執行。這絕對不是一個長期解決方案,因為目前還沒有辦法使用套件管理員或執行時期環境,例如 node.js。不過我們必定會在 wasm 執行緒穩定之前先將所有這些細節完善!
示範:光線追蹤
呼!提供了相當多資訊與背景知識,但希望您已對執行緒提案有一個更完善的想法,並了解我們可以在 Rust 和 wasm-bindgen
中如何加以善用。現在讓我們來看看精華部分。
我們最初努力想使用 Rayon 產生一個精美的 Mandelbrot 集合 渲染,但無奈 Web 工作執行緒衍生的限制讓我們 無法使用 Rayon。再加上我自己注意力不集中的毛病,以及對於 Mandelbrot 的認識不足,我轉而採取光線追蹤!
由於多年未接觸光線追蹤,我在 Google 上搜尋有無現成的 Rust 光線追蹤器可以試用。我最喜歡的一款很可惜需要用到夜間模式,而且上次編譯是在 2017 年中,不過 @bheisler 在 2017 年初提過一篇很棒的教學,而相關程式碼至今仍能編譯運作(太棒了!)。在進行一些微不足道的修改後,我就能直接在網路上使用這個專案。
順帶一提,這是 Rust 和 Cargo 最棒的地方之一。我很快就找到光線追蹤器,並將其整合、編譯為 Wasm,然後在瀏覽器中執行。
使用光線追蹤(或者至少這個光線追蹤器是如何運作的)時,由於影像中的每個畫素都彼此獨立地顯示,因此這會成為一個令人難堪的平行作業。也因此,我們可以想辦法讓在工作執行緒間分配畫素。
我認為這項示範中最後一個很酷的功能,是看到漸進式顯示,能看到影像在被渲染時的樣子。執行緒會在主執行緒要求更新工作執行緒時不時傳送一個ImageData
到主執行緒,並顯示在畫布上
看這項示範時請記得,這使用很多不穩定的夜間技術。在撰寫這篇文章時,它只能在 Firefox 上執行,因為其他瀏覽器尚未實作memory.init
指令。
你會在左側看到一個巨大的 JSON 資料區塊,這是要顯示場景的描述。現在這個光線追蹤器非常簡單,因此只支援平面和球體,但你可以移動物件,新增球體等等。如果你願意,我很樂意取得一些協助,實作更複雜的顯示效果。
未來的工作
雖然我們已經達到可以製作示範的階段,但是仍有許多工作要做!這是一些尚待執行的任務重點。
主執行緒不允許atomic.wait
瀏覽器的主執行緒無法執行atomic.wait
指令,如果執行,它會強制拋出例外。這表示,預設情況下,互斥鎖在主執行緒上競爭時將無法工作!此外,這也表示目前唯一與主執行緒同步的方法是postMessage
在工作執行緒中。
這個情況特別因為 Rust 的全域配置器 dlmalloc 全域同步而惡化。這表示如果主執行緒配置記憶體,當競爭時它偶爾可能會拋出例外!這實際上也是上述當前範例中的錯誤!
我在執行緒提案儲存庫中 開啟一個問題 來討論此事,希望我們能找出一個合理的方法讓主執行緒至少還能配置記憶體!到目前為止,我學到了一個 Atomics.waitAsync
提案,一種叫醒主執行緒的第二個機制。還有一些關於自訂配置器會有許多鎖的思考,但主執行緒在競爭期間會回落到 memory.grow
。不過在這同時,這使得主執行緒要使用 crates.io 上的任意程式庫變得非常困難,因為必須稽核 任何 同步。
我們可能為 Rust 實作一種「解決方法」,如果執行緒 ID 為 0,則在 mutex 實作中會自旋而不是進行 atomic.wait
。除了是同步的糟糕方式之外,它也硬性規定第一次的執行實例總是在主執行緒,這不一定總是正確!
未實作執行緒結束
現在 Rust 的模型中沒有執行緒結束的概念。這表示如果一個執行緒實際上結束了(又名作業執行緒被垃圾回收),那麼它會洩漏記憶體配置,如下所示
- 執行緒的堆疊(它從未被回收或重複使用)
- 執行緒局部儲存體中的所有資料(Rust 未註冊解構函數)
最終我們需要加入執行緒結束的概念,以便我們可以正確處理這個情況,並在稍後回收資源以重複使用。這可能是 WeakRef
提案 可以幫助解決的情況,因為它可以在 JavaScript 物件被垃圾回收時自動執行執行緒結束。
堆疊溢位再次發生
在配置線性記憶體時,LLD 預設會先配置靜態資料,然後再配置主執行緒的堆疊。不過這樣會有一個問題,如果主執行緒有堆疊溢位的情況,它會靜默地損壞所有靜態資料!要修正這個問題,我們會將 --stack-first
傳遞給 LLD,那樣一來,會將堆疊配置在記憶體中,由於記憶體存取超出範圍,會導致堆疊溢位並觸發陷阱。
不過,我們很不幸地沒有辦法在所有作業執行緒中使用這個方法。作業執行緒會遇到和先前相同的問題,萬一發生堆疊溢位,它會靜默地損壞堆積或靜態資料。
我們可以插入序言(由 LLVM 或 wasm-bindgen
)來檢查在減少堆疊指針之前我們是否有足夠的線性堆疊空間(如果沒有,則會陷入停滯),但尚不清楚何種效能影響這種變更對所有函數造成影響!其他解決方案可能需要新的 wasm 功能,例如解除映射記憶體以強制執行陷阱。
你也可以製作香腸喔!
如果你好奇香腸是如何製作的和/或你可以如何協助,以下列出了一些製造此範例 demo 的變更清單和一些有用的儲存庫!
- 精煉了大量記憶體作業概觀,與語意和編碼相關
wabt
組合現在支援大量記憶體操作指令parity-wasm
組合現在支援大量記憶體操作- 同步 Rust 標準函式庫中的全域配置器
- 為 Rust 標準函式庫實作執行緒本地儲存
- 修復產生的
wasm-bindgen
JS,也就是 API 不存在(網頁工作執行緒中的實例化必要條件) - 切換成使用執行緒本地
global()
的快取在js-sys
中 - 將
#[derive(Debug, Clone)]
新增至js_sys::Promise
- 將
TypedArray.slice
繫結新增至js-sys
- 確保
JsValue
並非Send
- 修復主執行緒上的未來狀態,避免沒有完成的情形
- Rust 標準函式庫中的 wasm32 原子初始實作
- 在
libm
中最佳化 wasm32 上的內聯函式 - Firefox 對
memory.init
的解碼需要更新 - 在
wasm-bindgen
中實作對 WebAssembly 執行緒的支援
如果你已經看到這裡,那麼你可能可以得知 Rust 中的執行緒故事仍然需要一些時間!我們很希望獲得您的協助,並且歡迎您在 Mozilla 的 IRC 上到 #rust-wasm
走走,#wg-wasm
在 Discord 上,或在 GitHub 上透過 wasm-bindgen
或 wasm-pack
追蹤。