简介
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 workerw.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 memoryw.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 primesvar ia = new Int32Array(sab); // ia.length == 100000var 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 primeAtomics.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 191ia[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 191Atomics.store(ia, 37, 123456); // was 163while (Atomics.load(ia, 37) == 163) ;console.log(ia[37]); // Will print 123456console.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 163Atomics.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的博客
欢送关注我的公众号:「程序那些事」最艰深的解读,最粗浅的干货,最简洁的教程,泛滥你不晓得的小技巧等你来发现!