C++20 でのアトミック参照

Atomics は、C++20 でいくつかの重要な拡張機能を受け取ります。今日は、新しいデータ型 std::atomic_ref. から始めます

タイプ std::atomic_ref アトミック操作を参照オブジェクトに適用します。

std::atomic_ref

std::atomic_ref を使用した同時書き込みと読み取り はデータ競合ではありません。参照されるオブジェクトの有効期間は、std::atomic_ref の有効期間を超えている必要があります . std::atomic_ref を使用して参照オブジェクトのサブオブジェクトにアクセスする は明確に定義されていません。

モチベーション

アトミック内で参照を使用するとうまくいくと思うかもしれません。残念ながら違います。

次のプログラムでは、クラス ExpensiveToCopy を持っています counter を含む . counter いくつかのスレッドで同時にインクリメントされます。したがって、counter 保護する必要があります。

// atomicReference.cpp

#include <atomic>
#include <iostream>
#include <random>
#include <thread>
#include <vector>

struct ExpensiveToCopy {
 int counter{};
};
 
int getRandom(int begin, int end) { // (6)

 std::random_device seed; // initial seed
 std::mt19937 engine(seed()); // generator
 std::uniform_int_distribution<> uniformDist(begin, end);

 return uniformDist(engine);
}
 
void count(ExpensiveToCopy& exp) { // (2)
 
 std::vector<std::thread> v;
 std::atomic<int> counter{exp.counter}; // (3)
 
 for (int n = 0; n < 10; ++n) { // (4)
 v.emplace_back([&counter] {
 auto randomNumber = getRandom(100, 200); // (5)
 for (int i = 0; i < randomNumber; ++i) { ++counter; }
 });
 }
 
 for (auto& t : v) t.join();

}

int main() {

 std::cout << std::endl;

 ExpensiveToCopy exp; // (1)
 count(exp);
 std::cout << "exp.counter: " << exp.counter << '\n';

 std::cout << std::endl;
 
}

exp (1) は、コピーにコストがかかるオブジェクトです。パフォーマンス上の理由から、関数 count (2) exp かかります 参考までに。 count std::atomic<int> を初期化します exp.counter ( で 3)。次の行は 10 個のスレッド (4) を作成し、それぞれが counter かかるラムダ式を実行します。 参考までに。ラムダ式は、100 から 200 (5) の間の乱数を取得し、正確に同じ頻度でカウンターをインクリメントします。関数 getRandom (6) 初期シードから開始し、乱数ジェネレータ Mersenne Twister を介して一様分散数を作成します。

結局、exp.counter (7) 10 個のスレッドが平均 150 回インクリメントされるため、おおよその値は 1500 になるはずです。 Wandbox オンライン コンパイラでプログラムを実行すると、驚くべき結果が得られます。

カウンターが 0 です。何が起こっていますか?問題は行 (3) にあります。式 std::atomic<int> counter{exp.counter} の初期化 コピーを作成します。次の小さなプログラムは、この問題を例示しています。

// atomicRefCopy.cpp

#include <atomic>
#include <iostream>

int main() {
 
 std::cout << std::endl;

 int val{5};
 int& ref = val; // (2)
 std::atomic<int> atomicRef(ref);
 ++atomicRef; // (1)
 std::cout << "ref: " << ref << std::endl;
 std::cout << "atomicRef.load(): " << atomicRef.load() << std::endl;
 
 std::cout << std::endl;

}

インクリメント操作 (1) は参照 ref をアドレス指定しません (2)。 ref の値 は変更されません。

std::atomic<int> counter{exp.counter} の置き換え std::atomic_ref<int> counter{exp.counter で } は問題を解決します:

// atomicReference.cpp

#include <atomic>
#include <iostream>
#include <random>
#include <thread>
#include <vector>

struct ExpensiveToCopy {
 int counter{};
};
 
int getRandom(int begin, int end) {

 std::random_device seed; // initial randomness
 std::mt19937 engine(seed()); // generator
 std::uniform_int_distribution<> uniformDist(begin, end);

 return uniformDist(engine);
}
 
void count(ExpensiveToCopy& exp) {
 
 std::vector<std::thread> v;
 std::atomic_ref<int> counter{exp.counter};
 
 for (int n = 0; n < 10; ++n) {
 v.emplace_back([&counter] {
 auto randomNumber = getRandom(100, 200);
 for (int i = 0; i < randomNumber; ++i) { ++counter; }
 });
 }
 
 for (auto& t : v) t.join();

}

int main() {

 std::cout << std::endl;

 ExpensiveToCopy exp;
 count(exp);
 std::cout << "exp.counter: " << exp.counter << '\n';

 std::cout << std::endl;
 
}

さて、counter の値 期待どおりです:

アトミックかどうか

そもそもなぜカウンターをアトミックにしなかったのかと聞かれるかもしれません:

struct ExpensiveToCopy {
 std::atomic<int> counter{};
};

もちろん、これは有効なアプローチですが、このアプローチには大きな欠点があります。カウンターの各アクセスは同期され、同期は無料ではありません。逆に、std::atomic_ref<int> counter を使用すると カウンターへのアトミック アクセスが必要な場合を明示的に制御できます。おそらく、ほとんどの場合、カウンターの値を読み取りたいだけでしょう。したがって、アトミックとして定義することはペシミゼーションです。

クラス テンプレート std::atomic_ref の詳細をいくつか紹介して、投稿を締めくくります。 .

std::atomic_ref の特殊化

std::atomic_ref を専門化できます ユーザー定義型の場合、ポインター型には部分的な特殊化を使用するか、整数型や浮動小数点型などの算術型には完全な特殊化を使用します。

プライマリ テンプレート

プライマリ テンプレート std::atomic_ref 自明にコピー可能な型 T でインスタンス化できます。自明にコピー可能な型は、スカラー型 (算術型、enum' s、ポインター、メンバー ポインター、または std::nullptr_t の)、または簡単にコピー可能なクラスとスカラー型の配列

ポインター型の部分的な特殊化

標準は、ポインター型の部分的な特殊化を提供します: std::atomic_ref<t*> .

算術型の特殊化

標準では、整数型と浮動小数点型の特殊化が提供されています:std::atomic_ref<arithmetic type> .

  • 文字タイプ:char, char8_t (C++20)、char16_t、char32_t、および wchar_t
  • 標準の符号付き整数型:signed char, short, int, long, 長い長い
  • 標準の符号なし整数型:unsigned char, unsigned short, unsigned int, unsigned long 、および unsigned long long
  • ヘッダー <cstdint> で定義された追加の整数型
  • 標準の浮動小数点型:floatdouble 、および long double

すべてのアトミック操作

まず、std::atomic_ref に対するすべての操作のリストを次に示します。 .

複合代入演算子 (+=, -=, |=, &= 、または ^= ) 新しい値を返します。 fetch バリエーションは古い値を返します。 compare_exchange_strong そして compare_exchange_weak アトミックな exchange を実行します 等しい場合、アトミック load そうでない場合。 true を返します 成功の場合、それ以外の場合は false .各関数は、追加のメモリ順序付け引数をサポートしています。デフォルトは順次一貫性です。

もちろん、すべての操作が std::atomic_ref. によって参照されるすべての型で利用できるわけではありません。 この表は、std::atomic_ref によって参照されるタイプに応じて、すべてのアトミック操作のリストを示しています。 .

最後の 2 つの表を注意深く調べると、std::atomic_ref を使用できることがわかります。 スレッドを同期します。

次は?

std::atomicstd::atomic_ref C++20 メンバー関数のサポート notify_onenotify_all 、および wait. 3 つの関数は、スレッドを同期するための便利な方法を提供します。次回の投稿では、std::atomic について詳しく見ていきます。 特に、std::atomic とのスレッド同期 の