ES8引入了SharedArrayBuffer和Atomics,經過共享內存來提高workers之間或者worker和主線程之間的消息傳遞速度。java
本文將會詳細的講解SharedArrayBuffer和Atomics的實際應用。node
在nodejs中,引入了worker_threads模塊,能夠建立Worker. 而在瀏覽器端,能夠經過web workers來使用Worker()來建立新的worker。程序員
這裏咱們主要關注一下瀏覽器端web worker的使用。web
咱們看一個常見的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的概念。post
咱們能夠經過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 }
上面的這個例子中,消息並無進行序列化或者轉換,都使用的是共享內存。
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 }
上面咱們獲取到了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的操做。
咱們來使用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的博客
歡迎關注個人公衆號:「程序那些事」最通俗的解讀,最深入的乾貨,最簡潔的教程,衆多你不知道的小技巧等你來發現!