乐趣区

关于es8:深入理解ES8的新特性SharedArrayBuffer

简介

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 的博客

欢送关注我的公众号:「程序那些事」最艰深的解读,最粗浅的干货,最简洁的教程,泛滥你不晓得的小技巧等你来发现!

退出移动版