web-dev-qa-db-ja.com

std :: atomicとは正確には何ですか?

std::atomic<>はアトミックオブジェクトであることを理解しています。しかし、どの程度アトミックですか?私の理解では、操作はアトミックになります。オブジェクトをアトミックにするとはどういう意味ですか?たとえば、次のコードを同時に実行する2つのスレッドがある場合:

a = a + 12;

次に、操作全体(たとえばadd_twelve_to(int))はアトミックですか?または、変数アトミックに変更が加えられましたか(そうoperator=())?

98
user4386938

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

アトミックタイプのオブジェクトは、データの競合がない唯一のC++オブジェクトです。つまり、あるスレッドがアトミックオブジェクトに書き込み、別のスレッドがそのオブジェクトから読み取る場合、動作は明確に定義されています。

さらに、アトミックオブジェクトへのアクセスは、スレッド間同期を確立し、std::memory_orderで指定された非アトミックメモリアクセスを順序付けます。

std::atomic<>は、C++より前では11回、MSVCで インターロックされた関数 を使用して実行する必要があった操作をラップします。GCCの場合は atomic bultins .

また、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;

単一のアトミックopには評価されません。結果としてa.load()(アトミック自体)が生成され、この値と最終結果の12およびa.store()(アトミック)の間に追加されます。前述したように、ここではstd::memory_order_seq_cstが使用されます。

ただし、a += 12を記述した場合、これはアトミック操作(前述のとおり)であり、a.fetch_add(12, std::memory_order_seq_cst)とほぼ同等です。

あなたのコメントに関して:

通常のintにはアトミックなロードとストアがあります。 atomic<>でラップすることのポイントは何ですか?

あなたの声明は、ストアやロードの原子性を保証するアーキテクチャにのみ当てはまります。これを行わないアーキテクチャがあります。また、通常、Word/dwordでアラインされたアドレスで操作をアトミックにする必要がありますstd::atomic<>は、追加要件なしでeveryプラットフォームでアトミックであることが保証されています。さらに、次のようなコードを記述できます。

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

このメモリ順序でのストア操作は、release操作を実行します。現在のスレッドで読み取りまたは書き込みを並べ替えることはできません afterこのストア。 現在のスレッドのすべての書き込みは、同じアトミック変数を取得する他のスレッドで表示されます

  • sharedDataは、whileループが終了した後に使用されるため、load() fromフラグがゼロ以外の値を返します。 load()std::memory_order_acquire順序を使用します。

std::memory_order_acquire

このメモリ順序でのロード操作は、影響を受けるメモリ位置でacquire操作を実行します。現在のスレッドの読み取りまたは書き込みは並べ替えできませんbeforeこのロード。 同じアトミック変数を解放する他のスレッドでのすべての書き込みは、現在のスレッドに表示されます

これにより、同期を正確に制御できるようになり、コードの動作の有無を明示的に指定できるようになります。原子性そのものだけが保証されている場合、これは不可能です。特に、 release-consume ordering のような非常に興味深い同期モデルに関しては。

111
Mateusz Grzejek

std::atomic<>はオブジェクトをアトミックにすることを理解しています。

それは視点の問題です...任意のオブジェクトに適用してその操作をアトミックにすることはできませんが、(ほとんどの)整数型とポインターに提供されている特殊化を使用できます。

a = a + 12;

std::atomic<>は(テンプレート式を使用して)これを単一のアトミック操作に単純化しませんが、代わりにoperator T() const volatile noexceptメンバーがaのアトミックload()を実行し、12が追加され、operator=(T t) noexceptstore(t)を実行します。

16
Tony Delroy