问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

Node.js 中的多线程:使用原子进行安全的共享内存操作

创作时间:
作者:
@小白创作中心

Node.js 中的多线程:使用原子进行安全的共享内存操作

引用
CSDN
1.
https://blog.csdn.net/weixin_48967543/article/details/141748626

Node.js 开发人员已经习惯了使用单线程执行 JavaScript。即使通过 引入多线程
worker_threads
,您仍会感到相当安全。
然而,当你为多个线程添加共享资源时,情况就不同了。事实上,这是整个软件工程中最具挑战性的主题之一。我说的是多线程编程。
值得庆幸的是,JavaScript 提供了一种内置抽象来缓解跨多线程共享资源的问题。这种机制称为Atomics。
在本文中,您将了解 Node.js 中的共享资源是什么样的,以及
Atomics
API 如何帮助我们防止野蛮竞争条件。

多个线程之间共享内存

让我们首先了解什么是可转移对象。
可转移对象是可以从一个执行上下文转移到另一个执行上下文而无需保留原始上下文的资源的对象。
执行上下文是可以执行 JavaScript 代码的地方。为了便于理解,我们假设执行上下文等于工作线程,因为每个线程确实是单独的执行上下文。
编辑
例如,是一个可传输对象。它由两部分组成:原始分配的内存和指向该内存的 JavaScript 句柄。您可以阅读有关JavaScript 中的缓冲区
ArrayBuffer
的文章以了解有关此主题的更多信息。
每当我们
ArrayBuffer
从主线程转到工作线程时,两个组件、原始内存和 JavaScript 对象都会在工作线程中重新创建。您无法访问工作
ArrayBuffer
线程内部的相同对象引用或底层内存。
不同线程之间共享资源的唯一方法是使用
SharedArrayBuffer

顾名思义,它被设计为共享。我们认为这个缓冲区是一个不可转移的对象。如果你尝试
SharedArrayBuffer
从主线程传递到工作线程,则只会重新创建 JavaScript 对象,但它引用的内存区域是相同的
编辑
虽然
SharedArrayBuffer
它是一个独特且强大的 API,但它是有成本的。
正如本叔叔告诉我们的那样:

当我们在多个线程之间共享资源时,我们将自己暴露在一个全新的充满恶劣竞争条件的世界中。

共享资源的竞争条件

通过一个具体的例子来理解我所说的内容会更容易。

  
import { Worker, isMainThread } from 'node:worker_threads';
if (isMainThread) {
  new Worker(import.meta.filename);
  new Worker(import.meta.filename);
} else {
  // worker code
}
  

我们使用同一个文件来运行主线程和工作线程。 条件下的块
isMainThread
仅针对主线程执行。 您可能还注意到
import.meta.filename
,它是自 Node 20.11.0 以来可用的变量的 ES6 替代品
__filename
。 接下来,我们介绍共享资源和对共享资源的操作。
复制
复制

  
import { Worker, isMainThread, workerData, threadId } from 'node:worker_threads';
if (isMainThread) {
  const buffer = new SharedArrayBuffer(1);
  new Worker(import.meta.filename, { workerData: buffer });
  new Worker(import.meta.filename, { workerData: buffer });
} else {
  const typedArray = new Int8Array(workerData);
  typedArray[0] = threadId;
  console.dir({ threadId, value: typedArray[0] });
}
  

我们将 传递
SharedArrayBuffer
给每个 worker
workerData
。两个 worker 都将缓冲区的第一个元素更改为其 ID。然后,我们记录第一个缓冲区元素。
其中一个工作者的 ID 等于 ,
1
另一个工作者的 ID 等于
2
。无需进一步阅读,当此代码运行时,您期望在输出中看到什么?
结果如下。
复制
复制

  
# 1 type of results
{ threadId: 1, value: 2 }
{ threadId: 2: value: 2 }
# 2 type of results
{ threadId: 1, value: 1 }
{ threadId: 2: value: 1 }
# 3 type of results
{ threadId: 1, value: 1 }
{ threadId: 2: value: 2 }
  

你注意到了吗?为什么会出现两个线程的值相同的情况?如果你从单线程程序的角度考虑,我们应该看到每次打印的值都不同。
即使我们在单个线程中异步运行此代码,唯一可能不同的是打印结果的顺序,但最终值的差异不会如此之大。
这里发生的情况是其中一个线程在这两行之间分配值:
复制
复制

  
  typedArray[0] = threadId;
  // one of the threads sneaks right in here and assign value
  console.dir({ threadId, value: typedArray[0] });
  

具体如下:

  1. 第一个线程为共享缓冲区分配一个值
  2. 第二个线程为共享缓冲区赋值
  3. 第一个线程将结果打印到控制台
  4. 第二个线程将结果打印到控制台。
    如您所见,当我们拥有共享资源和多个线程时,仅 10 行代码就很容易陷入竞争条件。这就是为什么我们需要一种机制来确保一个工作进程不会中断另一个工作进程的工作流程。该
    Atomics
    API 正是为此目的而创建的。

原子 www.cqzlsb.com

我想强调的是,使用是100% 确保在处理多个线程及其之间的共享资源时不会遇到竞争条件的唯一可能方法
Atomics

的主要目的
Atomics
是确保单个操作作为单个、不可中断的单元执行。换句话说,它确保没有其他工作者可以介入当前可执行的操作并执行他们的工作,就像我们之前看到的那样。
让我们使用 重写具有竞争条件的示例
Atomics

复制
复制

  
import { Worker, isMainThread, workerData, threadId } from 'node:worker_threads';
if (isMainThread) {
  const buffer = new SharedArrayBuffer(1);
  new Worker(import.meta.filename, { workerData: buffer });
  new Worker(import.meta.filename, { workerData: buffer });
} else {
  const typedArray = new Int8Array(workerData);
  const value = Atomics.store(typedArray, 0, threadId);
  console.dir({ threadId, value });
}
  

我们改变了两件事:保存值的方式和读取保存值的方式。使用
Atomics
,我们可以使用该函数同时执行这两个操作
store

当你运行此代码时,你不会看到两个线程具有相同值的情况。它们始终是不同的。
复制
复制

  
[1, 1]
[2, 2]
[2, 2]
[1, 1]
  

我们可以使用 2 个运算而不是 1 个:
store

load

复制
复制

  
const typedArray = new Int8Array(workerData);
Atomics.store(typedArray, 0, threadId);
const value = Atomics.load(typedArray, 0);
console.dir({ threadId, value });
  

但是,这种方法仍然容易出现竞争条件。使用 的目的
Atomics
是使我们的操作具有原子性。
在这种情况下,我们希望将 2 个操作作为单个原子操作执行:保存一个值并读取该值。当我们使用
store

load
函数时,我们实际上是在执行 2 个单独的原子操作,而不是 1 个。
这就是为什么仍然有可能陷入竞争状态,即一个工作者的代码介入
store
并被
load
其他线程调用。
不仅仅只有 2 个函数
Atomics
,在下一篇文章中,我们将介绍如何使用更多函数来构建我们自己的信号量和互斥锁,以使共享资源的工作更加方便。

结论

当只有一个线程时,Node.js 非常有趣和好用。如果在其上引入多个线程和共享资源,则会不可避免地出现竞争条件。
JavaScript 中只有一种机制可以让你缓解这些问题并避免竞争条件,它被称为
Atomics

其理念
Atomics
是让操作作为一个单独的单元执行,并且不会被外部打断。
由于这样的设计,我们可以确保无论何时使用
Atomics
函数,其他线程都无法进入此类操作的内部。

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号