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) 您希望使用WebAssembly
或 webgl
并希望优化性能
另外,如果你想创建一些复杂的 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 对象的实际用途是啥?的主要内容,如果未能解决你的问题,请参考以下文章
如何实现 SharedArrayBuffer 和 Atomics 的并行性?