簡介
ES8引入了SharedArrayBuffer和Atomics,通過共享記憶體來提升workers之間或者worker和主執行緒之間的訊息傳遞速度。
本文將會詳細的講解SharedArrayBuffer和Atomics的實際應用。
Worker和Shared memory
在nodejs中,引入了worker_threads模組,可以建立Worker. 而在瀏覽器端,可以通過web workers來使用Worker()來建立新的worker。
這裡我們主要關注一下瀏覽器端web worker的使用。
我們看一個常見的worker和主執行緒通訊的例子,主執行緒:
var w = new Worker("myworker.js")
w.postMessage("hi"); // send "hi" to the worker
w.onmessage = function (ev) {
console.log(ev.data); // prints "ho"
}
myworker的程式碼:
onmessage = function (ev) {
console.log(ev.data); // prints "hi"
postMessage("ho"); // sends "ho" back to the creator
}
我們通過postMessage來傳送訊息,通過onmessage來監聽訊息。
訊息是拷貝之後,經過序列化之後進行傳輸的。在解析的時候又會進行反序列化,從而降低了訊息傳輸的效率。
為了解決這個問題,引入了shared memory的概念。
我們可以通過SharedArrayBuffer來建立Shared memory。
考慮下上面的例子,我們可把訊息用SharedArrayBuffer封裝起來,從而達到記憶體共享的目的。
//傳送訊息
var sab = new SharedArrayBuffer(1024); // 1KiB shared memory
w.postMessage(sab)
//接收訊息
var sab;
onmessage = function (ev) {
sab = ev.data; // 1KiB shared memory, the same memory as in the parent
}
上面的這個例子中,訊息並沒有進行序列化或者轉換,都使用的是共享記憶體。
ArrayBuffer和Typed Array
SharedArrayBuffer和ArrayBuffer一樣是最底層的實現。為了方便程式設計師的使用,在SharedArrayBuffer和ArrayBuffer之上,提供了一些特定型別的Array。比如Int8Array,Int32Array等等。
這些Typed Array被稱為views。
我們看一個實際的例子,如果我們想在主執行緒中建立10w個質數,然後在worker中獲取這些質數該怎麼做呢?
首先看下主執行緒:
var sab = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 100000); // 100000 primes
var ia = new Int32Array(sab); // ia.length == 100000
var primes = new PrimeGenerator();
for ( let i=0 ; i < ia.length ; i++ )
ia[i] = primes.next();
w.postMessage(ia);
主執行緒中,我們使用了Int32Array封裝了SharedArrayBuffer,然後用PrimeGenerator來生成prime,儲存到Int32Array中。
下面是worker的接收:
var ia;
onmessage = function (ev) {
ia = ev.data; // ia.length == 100000
console.log(ia[37]); // prints 163, the 38th prime
}
併發的問題和Atomics
上面我們獲取到了ia[37]的值。因為是共享的,所以任何能夠訪問到ia[37]的執行緒對該值的改變,都可能影響其他執行緒的讀取操作。
比如我們給ia[37]重新賦值為123。雖然這個操作發生了,但是其他執行緒什麼時候能夠讀取到這個資料是未知的,依賴於CPU的排程等等外部因素。
為了解決這個問題,ES8引入了Atomics,我們可以通過Atomics的store和load功能來修改和監控資料的變化:
console.log(ia[37]); // Prints 163, the 38th prime
Atomics.store(ia, 37, 123);
我們通過store方法來向Array中寫入新的資料。
然後通過load來監聽資料的變化:
while (Atomics.load(ia, 37) == 163)
;
console.log(ia[37]); // Prints 123
還記得java中的重排序嗎?
在java中,虛擬機器在不影響程式執行結果的情況下,會對java程式碼進行優化,甚至是重排序。最終導致在多執行緒併發環境中可能會出現問題。
在JS中也是一樣,比如我們給ia分別賦值如下:
ia[42] = 314159; // was 191
ia[37] = 123456; // was 163
按照程式的書寫順序,是先給42賦值,然後給37賦值。
console.log(ia[37]);
console.log(ia[42]);
但是因為重排序的原因,可能37的值變成123456之後,42的值還是原來的191。
我們可以使用Atomics來解決這個問題,所有在Atomics.store之前的寫操作,在Atomics.load傳送變化之前都會發生。也就是說通過使用Atomics可以禁止重排序。
ia[42] = 314159; // was 191
Atomics.store(ia, 37, 123456); // was 163
while (Atomics.load(ia, 37) == 163)
;
console.log(ia[37]); // Will print 123456
console.log(ia[42]); // Will print 314159
我們通過監測37的變化,如果發生了變化,則我們可以保證之前的42的修改已經發生。
同樣的,我們知道在java中++操作並不是一個原子性操作,在JS中也一樣。
在多執行緒環境中,我們需要使用Atomics的add方法來替代++操作,從而保證原子性。
注意,Atomics只適用於Int8Array, Uint8Array, Int16Array, Uint16Array, Int32Array or Uint32Array。
上面例子中,我們使用while迴圈來等待一個值的變化,雖然很簡單,但是並不是很有效。
while迴圈會佔用CPU資源,造成不必要的浪費。
為了解決這個問題,Atomics引入了wait和wake操作。
我們看一個應用:
console.log(ia[37]); // Prints 163
Atomics.store(ia, 37, 123456);
Atomics.wake(ia, 37, 1);
我們希望37的值變化之後通知監聽在37上的一個陣列。
Atomics.wait(ia, 37, 163);
console.log(ia[37]); // Prints 123456
當ia37的值是163的時候,執行緒等待在ia37上。直到被喚醒。
這就是一個典型的wait和notify的操作。
使用Atomics來建立lock
我們來使用SharedArrayBuffer和Atomics建立lock。
我們需要使用的是Atomics的CAS操作:
compareExchange(typedArray: Int8Array | Uint8Array | Int16Array | Uint16Array | Int32Array | Uint32Array, index: number, expectedValue: number, replacementValue: number): number;
只有當typedArray[index]的值 = expectedValue 的時候,才會使用replacementValue來替換。 同時返回typedArray[index]的原值。
我們看下lock怎麼實現:
const UNLOCKED = 0;
const LOCKED_NO_WAITERS = 1;
const LOCKED_POSSIBLE_WAITERS = 2;
lock() {
const iab = this.iab;
const stateIdx = this.ibase;
var c;
if ((c = Atomics.compareExchange(iab, stateIdx,
UNLOCKED, LOCKED_NO_WAITERS)) !== UNLOCKED) {
do {
if (c === LOCKED_POSSIBLE_WAITERS
|| Atomics.compareExchange(iab, stateIdx,
LOCKED_NO_WAITERS, LOCKED_POSSIBLE_WAITERS) !== UNLOCKED) {
Atomics.wait(iab, stateIdx,
LOCKED_POSSIBLE_WAITERS, Number.POSITIVE_INFINITY);
}
} while ((c = Atomics.compareExchange(iab, stateIdx,
UNLOCKED, LOCKED_POSSIBLE_WAITERS)) !== UNLOCKED);
}
}
UNLOCKED表示目前沒有上鎖,LOCKED_NO_WAITERS表示已經上鎖了,LOCKED_POSSIBLE_WAITERS表示上鎖了,並且還有其他的worker在等待這個鎖。
iab表示要上鎖的SharedArrayBuffer,stateIdx是Array的index。
再看下tryLock和unlock:
tryLock() {
const iab = this.iab;
const stateIdx = this.ibase;
return Atomics.compareExchange(iab, stateIdx, UNLOCKED, LOCKED_NO_WAITERS) === UNLOCKED;
}
unlock() {
const iab = this.iab;
const stateIdx = this.ibase;
var v0 = Atomics.sub(iab, stateIdx, 1);
// Wake up a waiter if there are any
if (v0 !== LOCKED_NO_WAITERS) {
Atomics.store(iab, stateIdx, UNLOCKED);
Atomics.wake(iab, stateIdx, 1);
}
}
使用CAS我們實現了JS版本的lock。
當然,有了CAS,我們可以實現更加複雜的鎖操作,感興趣的朋友,可以自行探索。
本文作者:flydean程式那些事
本文連結:http://www.flydean.com/es8-shared-memory/
本文來源:flydean的部落格
歡迎關注我的公眾號:「程式那些事」最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!