1. ホーム
  2. c++

[解決済み】std::atomicとは一体何ですか?

2022-04-06 15:43:34

質問

私は、次のことを理解しています。 std::atomic<> はアトミックなオブジェクトです。しかし、どこまでがアトミックなのでしょうか?私の理解では、演算はアトミックになり得ます。オブジェクトをアトミックにするというのは、具体的にはどういうことでしょうか?例えば、次のようなコードを2つのスレッドが同時に実行するとします。

a = a + 12;

では、全体の操作(例えば add_twelve_to(int) はアトミックですか?それとも変数に加えられた変更はアトミックなのでしょうか(だから operator=() )?

解決方法は?

の各インスタンス化および完全な特殊化 std::atomic<> は、異なるスレッドが未定義の動作を起こすことなく、同時に操作できる型(のインスタンス)を表します。

<ブロッククオート

アトム型のオブジェクトは、データ競合のない唯一のC++オブジェクトです。つまり、あるスレッドがアトム型オブジェクトに書き込みを行い、別のスレッドがそれを読み取る場合、その動作は適切に定義されます。

さらに、アトミックオブジェクトへのアクセスは、スレッド間同期を確立し、非アトミックメモリアクセスの順序を、以下のように指定することができます。 std::memory_order .

std::atomic<> は、C++ 11 以前の時代には (例えば) を使って実行しなければならなかった操作をラップします。 連動関数 をMSVCで使用したり アトミックバルタン GCCの場合

また std::atomic<> を使用すると、より多くの制御が可能になります。 メモリオーダー 同期と順序の制約を指定します。C++ 11のアトミックとメモリモデルについてもっと知りたい場合は、以下のリンクが役に立つかもしれません。

なお、典型的なユースケースでは、おそらく オーバーロードされた算術演算子 または 別のセット :

std::atomic<long> value(0);
value++; //This is an atomic op
value += 5; //And so is this

演算子の構文ではメモリの順番を指定できないため、これらの操作は std::memory_order_seq_cst これは、C++ 11 のすべてのアトミック操作のデフォルトの順序だからです。これは、すべてのアトミック操作の間の順次一貫性(グローバルな全順序)を保証するものです。

しかし、場合によっては、これが必要ないこともある(タダで手に入るものはない)ので、より明示的な形式を使用するのもよいでしょう。

std::atomic<long> value {0};
value.fetch_add(1, std::memory_order_relaxed); // Atomic, but there are no synchronization or ordering constraints
value.fetch_add(5, std::memory_order_release); // Atomic, performs 'release' operation

さて、あなたの例です。

a = a + 12;

は、単一のアトミックなオペとして評価されません。 a.load() (これはアトミックである)、そしてこの値との間の加算は 12a.store() (これも原子)の最終結果です。先ほども書いたように std::memory_order_seq_cst が使われます。

ただし a += 12 とほぼ等価であり、アトミックな操作になります(先に述べたとおり)。 a.fetch_add(12, std::memory_order_seq_cst) .

コメントについてですが

通常の int はアトミックなロードとストアを持ちます。でラップする意味は何でしょう? atomic<> ?

あなたの発言は、ストアやロードのアトミック性を保証するアーキテクチャにのみ当てはまります。そうでないアーキテクチャもあります。また、アトミックであるためには、通常、ワード/ワードアラインされたアドレスに対して演算が行われる必要があります。 std::atomic<> はアトミックであることが保証されているもので あらゆる プラットフォームで、追加の要件なしに しかも、こんなコードも書けるようになる。

void* sharedData = nullptr;
std::atomic<int> ready_flag = 0;

// Thread 1
void produce()
{
    sharedData = generateData();
    ready_flag.store(1, std::memory_order_release);
}

// Thread 2
void consume()
{
    while (ready_flag.load(std::memory_order_acquire) == 0)
    {
        std::this_thread::yield();
    }

    assert(sharedData != nullptr); // will never trigger
    processData(sharedData);
}

アサーション条件は常に真である (つまり、決してトリガーされない) ことに注意してください。 while ループが終了します。それは、以下の理由からです。

  • store() の後にフラグを実行します。 sharedData が設定されている(と仮定している generateData() は常に有用なものを返し、特に NULL ) を使用し std::memory_order_release の順番になります。

memory_order_release

このメモリ順序でのストア操作では、次のようになります。 リリース 操作: 現在のスレッドでの読み書きの順序を変更することはできません。 このストア 現在のスレッドにおけるすべての書き込みは、次のスレッドで見ることができます。 同じアトム変数を取得する他のスレッド

  • sharedData の後に使用されます。 while ループが終了した後、つまり load() フラグが0以外の値を返すようになります。 load() が使用します。 std::memory_order_acquire の順番になります。

std::memory_order_acquire

このメモリ順序でのロードオペレーションは、次のように実行されます。 取得 操作 の読み書きができません。 スレッドの順序を変更することができます 以前 このロードを 他のスレッドでのすべての書き込み 同じアトム変数を解放している現在の スレッド .

これにより、同期の正確な制御が可能になり、コードがどのように動作するか/しないか/するつもりか/しないかを明示的に指定することができるようになります。もし、アトミック性だけを保証するのであれば、これは不可能でしょう。特に、以下のような非常に興味深い同期モデルに関しては、このようになります。 リリースコンシューマーオーダー .