web-dev-qa-db-ja.com

アトミックは偽のストアに苦しむことができますか?

C++では、アトミックは偽のストアに苦しむことができますか?

たとえば、mおよびnがアトミックであり、最初はm = 5であると仮定します。スレッド1では

    m += 2;

スレッド2では

    n = m;

結果:nの最終値は5または7のいずれかになるはずですよね?しかし、偽って6になる可能性はありますか?疑似的に4か8か、それとも他の何かでしょうか?

つまり、C++メモリモデルは、スレッド1がこれを行ったかのように動作することを禁止しますか?

    ++m;
    ++m;

または、もっと奇妙なことに、これをしたかのように?

    tmp  = m;
    m    = 4;
    tmp += 2;
    m    = tmp;

参照: H.-J。Boehm&SV Adve、2008、 図1.(リンクをたどる場合は、論文のセクション1で、最初の箇条書き項目を参照してください:「提供される非公式の仕様沿って ...")

質問の代替形式

1つの回答(感謝)は、上記の質問が誤解されている可能性があることを示しています。参考になれば、代替形式の質問を次に示します。

プログラマーがスレッド1 スキップするに操作を伝えようとしたとします。

    bool a = false;
    if (a) m += 2;

C++メモリモデルは、実行時にスレッド1がこれを行ったかのように動作することを禁止しますか?

    m += 2; // speculatively alter m
    m -= 2; // oops, should not have altered! reverse the alteration

以前にリンクされたBoehmとAdveは、マルチスレッド実行は

  • 変数を投機的に変更しますが、
  • 後で、投機的な変更が不要になったことが判明したときに、変数を元の値に戻します。

コンパイル可能なサンプルコード

必要に応じて、実際にコンパイルできるコードをいくつか示します。

#include <iostream>
#include <atomic>
#include <thread>

// For the orignial question, do_alter = true.
// For the question in alternate form, do_alter = false.
constexpr bool do_alter = true;

void f1(std::atomic_int *const p, const bool do_alter_)
{
    if (do_alter_) p->fetch_add(2, std::memory_order_relaxed);
}

void f2(const std::atomic_int *const p, std::atomic_int *const q)
{
    q->store(
        p->load(std::memory_order_relaxed),
        std::memory_order_relaxed
    );
}

int main()
{
    std::atomic_int m(5);
    std::atomic_int n(0);
    std::thread t1(f1, &m, do_alter);
    std::thread t2(f2, &m, &n);
    t2.join();
    t1.join();
    std::cout << n << "\n";
    return 0;
}

このコードを実行すると、常に5または7が出力されます。 (実際、私が知る限り、実行すると常に7が出力されます。)しかし、何も表示されませんセマンティクス64、または8の出力を妨げます。

優れたCppreference.com states、 「アトミックオブジェクトにはデータ競合がない」といいのですが、このような状況では、どういう意味ですか?

間違いなく、これらすべてが意味論をよく理解していないことを意味します。あなたが質問に当てることができるどんな照明でもいただければ幸いです。

[〜#〜]回答[〜#〜]

@ Christophe、@ ZalmanStern、@ BenVoigtはそれぞれ、スキルを使って質問を照らします。彼らの答えは競争するのではなく協力する。私の意見では、読者は3つの答えすべてに注意する必要があります。 @ZalmanStern 2番目。 @BenVoigtは最後にまとめます。

31
thb

既存の回答は多くの良い説明を提供しますが、あなたの質問に直接回答することはできません。さあ行こう:

アトミックは偽のストアに苦しむことができますか?

はい。ただし、データ競合のないC++プログラムからは観察できません。

volatileのみが実際に追加のメモリアクセスを実行することを禁止されています。

c ++メモリモデルは、スレッド1がこれを行ったかのように動作することを禁止しますか?

++m;
++m;

はい、しかしこれは許可されています:

lock (shared_std_atomic_secret_lock)
{
    ++m;
    ++m;
}

それは許されるが愚かだ。より現実的な可能性はこれを回すことです:

std::atomic<int64_t> m;
++m;

memory_bus_lock
{
    ++m.low;
    if (last_operation_did_carry)
       ++m.high;
}

どこ memory_bus_lockおよびlast_operation_did_carryは、ポータブルC++では表現できないハードウェアプラットフォームの機能です。

メモリバスdoにあるペリフェラルは中間値を参照しますが、メモリバスロックを確認することでこの状況を正しく解釈できることに注意してください。ソフトウェアデバッガーは中間値を確認できません。

その他の場合、アトミック操作はソフトウェアロックによって実装できます。その場合、

  1. ソフトウェアデバッガーは中間値を表示でき、誤解を避けるためにソフトウェアロックを認識する必要があります。
  2. ハードウェア周辺機器は、ソフトウェアロックの変更、およびアトミックオブジェクトの中間値を確認します。周辺機器が2つの関係を認識するために、いくつかの魔法が必要になる場合があります。
  3. アトミックオブジェクトが共有メモリにある場合、他のプロセスは中間値を見ることができ、ソフトウェアロックを検査する方法がない可能性があります/ソフトウェアロックの個別のコピーがある可能性があります
  4. 同じC++プログラム内の他のスレッドがデータの競合を引き起こすような方法でタイプセーフを解除した場合(たとえば、memcpyを使用してアトミックオブジェクトを読み取る)、中間値を監視できます。正式には、それは未定義の動作です。

最後の重要なポイントです。 「投機的書き込み」は非常に複雑なシナリオです。条件の名前を変更すると、これがわかりやすくなります。

スレッド#1

if (my_mutex.is_held) o += 2; // o is an ordinary variable, not atomic or volatile
return o;

スレッド#2

{
    scoped_lock l(my_mutex);
    return o;
}

ここではデータ競合はありません。スレッド#1がミューテックスをロックしている場合、書き込みと読み取りは順序付けられません。 mutexがロックされていない場合、スレッドは順不同で実行されますが、どちらも読み取りのみを実行しています。

したがって、コンパイラーは中間値の表示を許可できません。このC++コードは正しい書き換えではありません。

o += 2;
if (!my_mutex.is_held) o -= 2;

コンパイラがデータ競合を発明したからです。ただし、ハードウェアプラットフォームがレースフリーの投機的書き込みのメカニズムを提供している場合(おそらくItanium?)、コンパイラーはそれを使用できます。したがって、C++コードでは見えなくても、ハードウェアは中間値を見る可能性があります。

中間値がハードウェアに表示されないようにする場合は、volatileを使用する必要があります(volatile read-modify-writeはアトミックではないため、おそらくアトミックに加えて)。 volatileを使用すると、書き込みどおりに実行できない操作を要求すると、誤ったメモリアクセスではなく、コンパイルエラーが発生します。

20
Ben Voigt

あなたのコードは、アトミックで fetch_add() を使用します。これにより、次の保証が与えられます。

Atomicallyは、現在の値を、値とargの算術加算の結果で置き換えます。操作は、読み取り-変更-書き込み操作です。メモリは注文の値に応じて影響を受けます。

セマンティクスは非常に明確です。操作の前はm、操作の後はm + 2です。操作はアトミックであるため、これらの2つの状態の間にあるものにアクセスするスレッドはありません。


編集:別の質問に関する追加要素

BoehmとAdveの発言が何であれ、C++コンパイラは次の標準句に従います。

1.9/5:整形式プログラムを実行する適合実装は、同じ観察可能な動作同じプログラムと同じ入力を持つ抽象マシンの対応するインスタンスの可能な実行の1つとして。

C++コンパイラが、投機的な更新がプログラムの監視可能な動作(別名5または7以外のものを取得する)に干渉する可能性があるコードを生成する場合、それは標準に準拠していません。最初の答え。

23
Christophe

改訂された質問は、順次一貫性から緩和されたメモリ順序に移行したという点で、最初の質問とはかなり異なります。

弱いメモリの順序付けについての推論と指定はどちらもかなり注意が必要です。例えば。ここで指摘されているC++ 11仕様とC++ 14仕様の違いに注意してください: http://en.cppreference.com/w/cpp/atomic/memory_order#Relaxed_ordering 。ただし、アトミック性の定義により、fetch_add呼び出しによって、他のスレッドが変数に書き込まれた値またはそれらにプラス2の値以外の値を参照することができなくなります(スレッドは、中間値が他のスレッドによって監視されないことを保証します。)

(ひどく具体的にするために、C++仕様で「read-modify-write」を検索する可能性があります。例: http://www.open-std.org/jtc1/sc22/wg21/docs/papers /2017/n4659.pdf 。)

おそらく、あなたが質問があるリンクされた論文の場所に特定の参照を与えることが役立つでしょう。その論文は、最初のC++並行メモリモデル仕様(C++ 11で)をほんの少しだけ古くしており、現在はそれを超えたもう1つの改訂版なので、標準が実際に言っていることに関しては少し古くなっている可能性もあります。これは、非原子変数で発生する可能性があることを提案することのより多くの問題であることを期待しています。

編集:「セマンティクス」についてもう少し追加して、おそらくこの種のことを分析する方法について考えるのを助けるでしょう。

メモリの順序付けの目的は、スレッド間での変数の読み取りと書き込みの間に可能な順序のセットを確立することです。弱い順序では、すべてのスレッドに適用される単一のグローバル順序があることは保証されません。これだけでもすでにトリッキーなので、次に進む前に完全に理解しておく必要があります。

順序の指定に関連する2つのことは、アドレスと同期操作です。実際には、同期操作には2つのサイドがあり、これらの2つのサイドはアドレスの共有を介して接続されます。 (フェンスはすべてのアドレスに適用されると考えることができます。)あるアドレスでの同期操作が他のアドレスに対して何かを保証する場合、スペースの混乱の多くは、考え出すことに起因します。例えば。ミューテックスのロックおよびロック解除操作は、ミューテックス内のアドレスに対する取得および解放操作を介してのみ順序付けを確立しますが、その同期はall読み取りおよび書き込みに適用されますmutexをロックおよびロック解除するスレッド。緩和された順序付けを使用してアクセスされるアトミック変数は、発生することに対してほとんど制約を課しませんが、それらのアクセスには、他のアトミック変数またはミューテックスに対するより強く順序付けられた操作によって課される順序付け制約がある場合があります。

主な同期操作はacquirereleaseです。参照: http://en.cppreference.com/w/cpp/atomic/memory_order 。これらは、ミューテックスで何が起こるかという名前です。取得操作はロードに適用され、取得が発生した時点を超えて現在のスレッドのメモリ操作が並べ替えられないようにします。また、同じ変数に対する以前のリリース操作の順序も確立します。最後のビットは、ロードされた値によって制御されます。つまりロードがリリース同期で指定された書き込みから値を返す場合、ロードはその書き込みに対して順序付けされ、それらのスレッドによる他のすべてのメモリ操作は順序付け規則に従って配置されます。

アトミック、つまり読み取り-変更-書き込みの操作は、大きな順序での独自の小さなシーケンスです。読み取り、操作、および書き込みがアトミックに行われることが保証されています。その他の順序付けは、メモリ順序パラメータによって操作に与えられます。例えば。緩和された順序を指定すると、他の変数には制約が適用されなくなります。つまりこの操作によって暗黙的に取得または解放されることはありません。 memory_order_acq_relを指定すると、操作がアトミックであるだけでなく、読み取りが取得であり、書き込みがリリースであることが示されます。スレッドがリリースセマンティクスで別の書き込みから値を読み取る場合、他のすべてのアトミックは適切なこのスレッドの順序制約。

メモリ順序が緩和されたfetch_addは、プロファイリングの統計カウンターに使用できます。操作の最後に、すべてのスレッドが何か他のことを行って、すべてのカウンターの増分が最終リーダーに表示されるようにしますが、中間状態では、最終合計が加算される限り、気にしません。ただし、これは、中間読み取りがカウントに含まれなかった値をサンプリングできることを意味するものではありません。例えば。常に0から始まるカウンターに偶数の値を追加する場合、順序に関係なく、どのスレッドも奇数の値を読み取ることはできません。

私は、プログラムで明示的にエンコードされたもの以外の原子変数には副作用がないと述べている標準の特定のテキストを指すことができないことに少し気が遠いです。多くのことが副作用について言及していますが、副作用はソースによって指定されたものであり、コンパイラによって構成されたものではないと当然のように思われます。現時点でこれを追跡する時間はありませんが、これが保証されていない場合は機能しないことがたくさんあります。std::atomicのポイントの一部は、この制約を取得することです。その他の変数。 (これはvolatileによって多少提供されるか、少なくとも提供されることを意図しています。std::atomicに関するメモリの順序付けについてこの程度の仕様がある理由の一部は、volatileが詳細について推論するのに十分に指定されており、1つのセットの制約がすべてのニーズを満たしていません。

5
Zalman Stern