1726358844
2024-09-13 08:19:52
Node.jsの開発者は、JavaScriptが実行される単一のスレッドに慣れすぎていました。 worker_threads、かなり安心できます。
ただし、複数のスレッドに共有リソースを追加すると状況は変わります。実際、これはすべてのソフトウェア エンジニアリングの中で最も難しいトピックの 1 つです。ここでは、マルチスレッド プログラミングについて説明します。
ありがたいことに、JavaScriptは複数のスレッド間でリソースを共有する問題を軽減するための組み込みの抽象化を提供しています。このメカニズムは アトミック。
この記事では、Node.jsで共有リソースがどのように見えるか、そしてどのように Atomics API は、乱暴な競合状態を防ぐのに役立ちます。
複数のスレッド間での共有メモリ
まず、転送可能なオブジェクトとは何かを理解することから始めましょう。
転送可能なオブジェクトは、元のコンテキストのリソースを保持せずに、ある実行コンテキストから別の実行コンテキストに転送できるオブジェクトです。
実行コンテキストは、JavaScript コードを実行できる場所です。理解しやすくするために、各スレッドは実際には個別の実行コンテキストであるため、実行コンテキストはワーカー スレッドと同じであると仮定します。
例えば、 ArrayBuffer 転送可能なオブジェクトです。これは2つの部分で構成されています。割り当てられた生のメモリと、このメモリへのJavaScriptハンドルです。 JavaScript のバッファ このトピックについてさらに詳しく知るには。
移転するたびに 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 {
}
メインスレッドとワーカースレッドを実行するために同じファイルを使用しています。 isMainThread 条件はメインスレッドでのみ実行されます。また、 import.meta.filenameES6の代替品です __filename 変数は Node 20.11.0 以降で使用できます。次に、共有リソースと、共有リソースに対する操作を紹介します。
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 労働者一人一人に workerData両方のワーカーは、バッファの最初の要素をその ID に変更します。次に、最初のバッファ要素をログに記録します。
作業員の1人のIDは次のようになります 1 そしてもう1つは 2これ以上読まずに、このコードを実行したときに出力に何が表示されると予想しますか?
結果は次のとおりです。
{ threadId: 1, value: 2 }
{ threadId: 2: value: 2 }
{ threadId: 1, value: 1 }
{ threadId: 2: value: 1 }
{ threadId: 1, value: 1 }
{ threadId: 2: value: 2 }
気付きましたか?なぜ両方のスレッドで値が同じになるケースがあるのでしょうか?シングルスレッドプログラムの観点から考えてみると、 違う 毎回値が印刷されます。
このコードを単一のスレッドで非同期的に実行した場合でも、異なる可能性があるのは結果が印刷される順序のみであり、最終的な値にそれほど大きな違いはありません。
ここで起こることは、スレッドの 1 つが次の 2 行の間に値を割り当てることです。
typedArray[0] = threadId;
console.dir({ threadId, value: typedArray[0] });
それは次のようになります:
-
最初のスレッドは共有バッファに値を割り当てます
-
2番目のスレッドは共有バッファに値を割り当てる
-
最初のスレッドは結果をコンソールに出力します
-
2 番目のスレッドは結果をコンソールに出力します。
ご覧のとおり、共有リソースと複数のスレッドがある場合、わずか10行のコードで競合状態に陥るのは簡単です。そのため、1つのワーカーが別のワーカーのワークフローを中断しないようにするメカニズムが必要です。 Atomics API はまさにこの目的のために作成されました。
アトミック
強調したいのは、 Atomics は 唯一の可能な方法 複数のスレッドとそれらの間の共有リソースを扱うときに競合状態が発生しないことを 100% 確実にします。
の主な目的は 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 });
}
値の保存方法と保存した値の読み取り方法の2点を変更しました。 Atomics両方の操作を同時に実行するために、 store 関数。
このコードを実行すると、両方のスレッドが同じ値を持つケースは表示されません。常に異なります。
[1, 1]
[2, 2]
[2, 2]
[1, 1]
1 つの操作ではなく 2 つの操作を使用することもできます。 store そして load。
const typedArray = new Int8Array(workerData);
Atomics.store(typedArray, 0, threadId);
const value = Atomics.load(typedArray, 0);
console.dir({ threadId, value });
しかし、このアプローチは依然として競合状態になりやすい。 Atomics 私たちの事業を 原子。
この場合、値の保存と値の読み取りという2つの操作を1つのアトミック操作として実行します。 store そして load 関数では、実際には 1 つのアトミック操作ではなく、 2 つの個別のアトミック操作を実行しています。
そのため、あるワーカーのコードが他のワーカーのコードと競合する状態が発生する可能性が依然としてあります。 store そして load 他のスレッドからの呼び出し。
2つの機能だけではありません Atomics次の記事では、 より多くの機能を使用して独自のセマフォとミューテックスを構築する方法 共有リソースでの作業をさらに便利にします。
結論
Node.js は、スレッドが 1 つしかないときはとても楽しくて便利です。しかし、その上に複数のスレッドや共有リソースを導入すると、競合状態が避けられない環境になります。
JavaScriptには、これらの問題を軽減し、競合状態を回避するためのメカニズムが1つだけあります。それは Atomics。
の考え方 Atomics 外部から中断できない単一のユニットとして実行される操作を持つことです。
このようなデザインのおかげで、私たちはいつでも Atomics 関数では、他のスレッドがそのような操作の内部にアクセスする方法はありません。
#Atomics #を使用した #Node.js #でのマルチスレッド #プログラミング