アトミック

ブール値に加えて、ポインター、積分、およびユーザー定義型のアトミックがあります。ユーザー定義型の規則は特別です。

両方。ポインター T* std::atomic または整数型 integ std::atomic のアトミック ラッパーは、CAS (比較と交換) 操作を有効にします。

std::atomic

アトミック ポインター std::atomic は、プレーン ポインター T* のように動作します。そのため、 std::atomic は、ポインター演算と、前後のインクリメント操作または前後のデクリメント操作をサポートします。短い例を見てください。

int intArray[5];
std::atomic<int*> p(intArray);
p++;
assert(p.load() == &intArray[1]);
p+=1;
assert(p.load() == &intArray[2]);
--p;
assert(p.load() == &intArray[1]);

std::atomic<整数型>

C++11 には、既知の整数データ型に対するアトミック型があります。いつものように、en.cppreference.com ページで、アトミックな整数データ型に関するすべての情報 (操作を含む) を読むことができます。

複合代入演算子 +=、-=、&=、|=および ^=とそこに含まれる std::atomic<>::fetch_add()、std::atomic<>::fetch_sub()、std::atomic<>::fetch_and()、std::atomic<>::fetch_or()、および std::atomic<>::fetch_xor() は、最も興味深いものです。アトミックな読み取り操作と書き込み操作には少し違いがあります。複合代入演算子は新しい値を返し、フェッチ バリエーションは古い値を返します。深く見ると、より多くの洞察が得られます。アトミックな方法での乗算、除算、およびシフト操作はありません。しかし、それはそれほど大きな制限ではありません。これらの操作が必要になることは比較的少なく、簡単に実装できるためです。どのように?例を見てください。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// fetch_mult.cpp

#include <atomic>
#include <iostream>

template <typename T>
T fetch_mult(std::atomic<T>& shared, T mult){
 T oldValue= shared.load();
 while (!shared.compare_exchange_strong(oldValue, oldValue * mult));
 return oldValue;
}

int main(){
 std::atomic<int> myInt{5};
 std::cout << myInt << std::endl; 
 fetch_mult(myInt,5);
 std::cout << myInt << std::endl; 
}

一点申し上げておきたいことがあります。 9 行目の追加は、oldValue ==shared という関係が成り立つ場合にのみ発生します。したがって、乗算が常に行われるようにするために、乗算を while ループに入れます。プログラムの結果はそれほどスリリングではありません。

関数テンプレート fetch_mult の実装は汎用的で、汎用的すぎます。したがって、任意の型で使用できます。数字の 5 の代わりに C 文字列 5 を使用すると、Microsoft コンパイラは呼び出しが曖昧であると文句を言います。

"5" は、const char* または int として解釈できます。それは私の意図ではありませんでした。テンプレート引数は整数型でなければなりません。コンセプトライトの適切なユースケース。コンセプト ライトでは、テンプレート パラメーターに制約を表現できます。悲しいことに、それらは C++17 の一部ではありません。 C++20 標準に期待する必要があります。

1
2
3
4
5
6
7
template <typename T>
 requires std::is_integral<T>::value
T fetch_mult(std::atomic<T>& shared, T mult){
 T oldValue= shared.load();
 while (!shared.compare_exchange_strong(oldValue, oldValue * mult));
 return oldValue;
}

述語 std::is_integral::value はコンパイラによって評価されます。 T が整数型でない場合、コンパイラは文句を言います。 std::is_integral は、C++11 の一部である新しい型特性ライブラリの関数です。 2 行目の必須条件は、テンプレート パラメーターの制約を定義します。コンパイラはコンパイル時にコントラクトをチェックします。

独自のアトミック タイプを定義できます。

std::atomic<ユーザー定義型>

アトミック型 std::atomic を取得するためのユーザー定義型には、多くの重大な制限があります。これらの制限は型に関するものですが、これらの制限は std::atomic が実行できる利用可能な操作に関するものです。

MyType には次の制限があります:

  • MyType、MyType のすべての基本クラス、および MyType のすべての非静的メンバーのコピー代入演算子は自明でなければなりません。コンパイラによって生成されたコピー代入演算子による自動のみが自明です。逆に言うと。ユーザー定義のコピー代入演算子は簡単ではありません。
  • MyType に仮想メソッドまたは基本クラスを含めてはなりません
  • C 関数 memcpy または memcmp を適用できるように、MyType はビットごとに比較可能でなければなりません。

コンパイル時に関数 std::is_trivially_copy_constructible、std::is_polymorphic、および std::is_trivial を使用して MyType の制約を確認できます。すべての関数は型特性ライブラリの一部です。

ユーザー定義型 std::atomic の場合、操作の縮小セットのみがサポートされます。

アトミック操作

全体像を把握するために、アトミック タイプに依存するアトミック操作を次の表に示しました。

無料のアトミック関数とスマート ポインター

クラス テンプレート std::atomic とフラグ std::atomic_flag の機能は、無料の関数として使用できます。フリー関数は参照の代わりにアトミック ポインターを使用するため、C と互換性があります。アトミック フリー関数は、クラス テンプレート std::atomic と同じ型をサポートしますが、それに加えて、スマート ポインター std::shared_ptr をサポートします。 std::shared_ptr は原子データ型ではないため、これは特別です。 C++ 委員会は、内部で参照カウンターとオブジェクトを維持するスマート ポインターのインスタンスがアトミックな方法で変更可能でなければならないという必要性を認識しました。

std::shared_ptr<MyData> p;
std::shared_ptr<MyData> p2= std::atomic_load(&p);
std::shared_ptr<MyData> p3(new MyData);
std::atomic_store(&p, p3);

明確にするために。 アトミック特性は参照カウンターに対してのみ保持され、オブジェクトに対しては保持されません。 それが理由で、将来的に std::atomic_shared_ptr を取得します (将来が C++17 と呼ばれるか C++20 と呼ばれるかはわかりません。過去にはしばしば間違っていました)。 std::shared_ptr であり、基礎となるオブジェクトの原子性を保証します。これは std::weak_ptr にも当てはまります。リソースの一時的な所有者である std::weak_ptr は、std::shared_ptr の循環的な依存関係を解消するのに役立ちます。新しいアトミック std::weak_ptr の名前は std::atomic_weak_ptr になります。全体像を完全なものにするために、std::unique_ptr のアトミック バージョンは std::atomic_unique_ptr と呼ばれます。

次は?

これで、アトミック データ型の基礎が築かれました。次の投稿では、アトミックの同期と順序付けの制約について説明します。