ECMAScript 中 Atomics 对象的实际用途是啥?

Posted

技术标签:

【中文标题】ECMAScript 中 Atomics 对象的实际用途是啥?【英文标题】:What's the actual use of the Atomics object in ECMAScript?ECMAScript 中 Atomics 对象的实际用途是什么? 【发布时间】:2018-02-02 21:10:40 【问题描述】:

ECMAScript specification 定义了 24.4 部分中的 Atomics 对象

在所有全局对象中,这对我来说比较模糊,因为直到我没有阅读它的规范之前我才知道它的存在,而且 Google 也没有太多引用它(或者名称可能太太通用了,一切都被淹没了?)。

根据其官方定义

Atomics 对象提供了在共享内存数组单元上不可分割地(原子地)操作的函数 以及让代理等待和调度原始事件的函数

因此,它具有对象的形状,具有多种方法来处理低级内存并调节对它的访问。它的公共界面也让我猜到了。但是,最终用户对此类对象的实际用途是什么?为什么是公开的?有没有一些有用的例子?

谢谢

【问题讨论】:

原子是 ES8 的一部分,而不是 ES6。 最终用户是什么意思? 2ality.com/2017/01/shared-array-buffer.html 和 tc39.github.io/ecmascript_sharedmem/shmem.html#intro 应该是一些不错的读物。不要搜索“原子”,而是尝试使用“共享内存”一词。 @Bergi 我知道!但是我没有足够的分数来创建标签,所以我使用了它。对于最终用户,我指的是制作现实世界应用程序的开发人员 @Bergi 谢谢你的链接! 【参考方案1】:

如果您有一些复杂的计算,您可能需要WebWorkers,以便您的主脚本在并行完成繁重的工作时继续其工作。

Atomics 解决的问题是 WebWorkers 如何在彼此之间进行通信(轻松、快速和可靠)。您可以阅读有关 ArrayBuffer、SharedArrayBuffer、Atomics 以及如何将它们用于您的好处here。

如果出现以下情况,您不应该为此烦恼:

您正在创建一些简单的东西(例如商店、论坛等)

如果出现以下情况,您可能需要它:

您想创建一些复杂或消耗内存的东西(例如 figma 或 google drive) 您希望使用 WebAssemblywebgl 并希望优化性能 另外,如果你想创建一些复杂的 Node.js 模块,你可能需要它 或者,如果您通过Electron 创建复杂的应用程序,例如Skype 或Discord

感谢 Pavlo Mur 和 Simon Paris 的回答!

【讨论】:

【参考方案2】:

Atomics 用于同步共享内存的 WebWorker。它们导致对 SharedArrayBuffer 的内存访问以线程安全的方式完成。共享内存使多线程更加有用,因为:

无需复制数据即可将其传递给线程 线程可以在不使用事件循环的情况下进行通信 线程可以更快地通信

例子:

var arr = new SharedArrayBuffer(1024);

// send a reference to the memory to any number of webworkers
workers.forEach(worker => worker.postMessage(arr));

// Normally, simultaneous access to the memory from multiple threads 
// (where at least one access is a write)
// is not safe, but the Atomics methods are thread-safe.
// This adds 2 to element 0 of arr.
Atomics.add(arr, 0, 2)

SharedArrayBuffer 之前在主流浏览器上启用,但在 Spectre incident 之后被禁用,因为共享内存允许实现纳秒精度的计时器,从而允许利用 Spectre。

为了确保安全,浏览器需要为每个域运行一个单独的进程。 Chrome 在 67 版中开始这样做,并且在 68 版中重新启用了共享内存。

【讨论】:

【参考方案3】:

除了 Arseniy-II 和 Simon Paris 所说的之外,当您将 javascript 引擎嵌入到某个主机应用程序中(以在其中启用脚本)时,Atomics 也很方便。然后,您可以同时从不同的并发线程直接访问共享内存,无论是从 JS 还是从 C/C++ 或您的主机应用程序编写的任何语言,而无需涉及用于 C/C++/OtherLanguage 端访问的 JavaScript API。

【讨论】:

【参考方案4】:

原子操作是“全有或全无”的一组较小的操作。

我们来看看

let i=0;

i++

i++ 实际上是用 3 个步骤评估的

    读取当前i值 将i 增加 1 返回旧值

如果您有 2 个线程执行相同的操作会怎样?它们都可以读取相同的值 1 并同时递增。

但是这个和Javascript,不是单线程的吗?

是的! JavaScript 确实是单线程,但浏览器/节点现在允许并行使用多个 JavaScript 运行时(工作线程、Web Worker)。

Chrome 和 Node(基于 v8)为每个线程创建 Isolate,它们都在自己的 context 中运行。

share memory 的唯一途径是通过ArrayBuffer / SharedArrayBuffer

下一个程序的输出是什么?

使用节点 > =10 运行(您可能需要--experimental_worker 标志)

node example.js

const  isMainThread, Worker, workerData  = require('worker_threads');

if (isMainThread) 
  // main thread, create shared memory to share between threads
  const shm = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT);

  process.on('exit', () => 
    // print final counter
    const res = new Int32Array(shm);
    console.log(res[0]); // expected 5 * 500,000 = 2,500,000
  );
  Array(5).fill(null).map(() => new Worker(__filename,  workerData: shm ));
 else 
  // worker thread, iteratres 500k and doing i++
  const arr = new Int32Array(workerData);
  for (let i = 0; i < 500000; i++) 
    arr[i]++;
  

输出可能是 2,500,000,但我们不知道,在大多数情况下,它不会是 2.5M,实际上,您获得相同输出的机会两次是相当低的,作为程序员,我们肯定不喜欢我们不知道它会如何结束的代码。

这是竞争条件的一个示例,其中 n 个线程相互竞争并且没有以任何方式同步。

Atomic 运算来了,它允许我们从头到尾进行算术运算。

让我们稍微改变一下程序,现在运行:

const  isMainThread, Worker, workerData  = require('worker_threads');


if (isMainThread) 
    const shm = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT);
    process.on('exit', () => 
        const res = new Int32Array(shm);
        console.log(res[0]); // expected 5 * 500,000 = 2,500,000
    );
    Array(5).fill(null).map(() => new Worker(__filename,  workerData: shm ));
 else 
    const arr = new Int32Array(workerData);
    for (let i = 0; i < 500000; i++) 
        Atomics.add(arr, 0, 1);
    

现在输出将始终为2,500,000

奖励,使用 Atomics 的互斥锁

有时候,我们希望一个操作只有一个线程可以同时访问,让我们看看下一个类

class Mutex 

    /**
     * 
     * @param Mutex mutex 
     * @param Int32Array resource 
     * @param number onceFlagCell 
     * @param (done)=>void cb
     */
    static once(mutex, resource, onceFlagCell, cb) 
        if (Atomics.load(resource, onceFlagCell) === 1) 
            return;
        
        mutex.lock();
        // maybe someone already flagged it
        if (Atomics.load(resource, onceFlagCell) === 1) 
            mutex.unlock();
            return;
        
        cb(() => 
            Atomics.store(resource, onceFlagCell, 1);
            mutex.unlock();
        );
    
    /**
     * 
     * @param Int32Array resource 
     * @param number cell 
     */
    constructor(resource, cell) 
        this.resource = resource;
        this.cell = cell;
        this.lockAcquired = false;
    

    /**
     * locks the mutex
     */
    lock() 
        if (this.lockAcquired) 
            console.warn('you already acquired the lock you stupid');
            return;
        
        const  resource, cell  = this;
        while (true) 
            // lock is already acquired, wait
            if (Atomics.load(resource, cell) > 0) 
                while ('ok' !== Atomics.wait(resource, cell, 0));
            
            const countOfAcquiresBeforeMe = Atomics.add(resource, cell, 1);
            // someone was faster than me, try again later
            if (countOfAcquiresBeforeMe >= 1) 
                Atomics.sub(resource, cell, 1);
                continue;
            
            this.lockAcquired = true;
            return;
        
    

    /**
     * unlocks the mutex
     */
    unlock() 
        if (!this.lockAcquired) 
            console.warn('you didn\'t acquire the lock you stupid');
            return;
        
        Atomics.sub(this.resource, this.cell, 1);
        Atomics.notify(this.resource, this.cell, 1);
        this.lockAcquired = false;
    

现在,您需要分配SharedArrayBuffer 并在所有线程之间共享它们,然后看到每次只有1 个线程进入critical section

使用节点 > 10 运行

node --experimental_worker example.js

const  isMainThread, Worker, workerData, threadId  = require('worker_threads');


const  promisify  = require('util');
const doSomethingFakeThatTakesTimeAndShouldBeAtomic = promisify(setTimeout);

if (isMainThread) 
    const shm = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT);
    Array(5).fill(null).map(() => new Worker(__filename,  workerData: shm ));
 else 
    (async () => 
        const arr = new Int32Array(workerData);
        const mutex = new Mutex(arr, 0);
        mutex.lock();
        console.log(`[$threadId] $new Date().toISOString()`);
        await doSomethingFakeThatTakesTimeAndShouldBeAtomic(1000);
        mutex.unlock();
    )();

【讨论】:

【参考方案5】:

我使用 Web Worker 和 SharedArrayBuffer 编写了一个脚本来演示 Atomics 的使用:

<!DOCTYPE html><html><head></head><body><script>
   var arr = new SharedArrayBuffer(256);
   new Int16Array(arr)[0]=0;
   var workers=[];
   for (let i=0; i<1000; i++) workers.push(new Worker('worker.js'));
   workers.forEach(w => w.postMessage(new Int16Array(arr)));
</script></body></html>

然后用一个单独的文件worker.js:

// worker.js
onmessage = function(e) 
    e.data[0]++;                 // last line is 981 only? wth?!
    //Atomics.add(e.data,0,1);   // last line is exactly 1000. right...
    console.log(e.data[0]);

如您所见,如果没有 Atomics 保证的互斥锁,有时无法正确执行加法。

【讨论】:

以上是关于ECMAScript 中 Atomics 对象的实际用途是啥?的主要内容,如果未能解决你的问题,请参考以下文章

ECMAScript5.1的运算符类型转换总结

如何实现 SharedArrayBuffer 和 Atomics 的并行性?

var let const 区别

var let const 区别

C++ 11 可以在不同线程中通过引用安全地传递和访问 std::atomics

reentrantlock与synchronized的区别.及Atomics的使用