31

アラインされたポインターのロードとストアがターゲット プラットフォーム上で自然にアトミックであると仮定すると、次の違いは何ですか。

// Case 1: Dumb pointer, manual fence
int* ptr;
// ...
std::atomic_thread_fence(std::memory_order_release);
ptr = new int(-4);

これ:

// Case 2: atomic var, automatic fence
std::atomic<int*> ptr;
// ...
ptr.store(new int(-4), std::memory_order_release);

この:

// Case 3: atomic var, manual fence
std::atomic<int*> ptr;
// ...
std::atomic_thread_fence(std::memory_order_release);
ptr.store(new int(-4), std::memory_order_relaxed);

それらはすべて同等であるという印象を受けましたが、Relacyは最初のケース (のみ) でデータ競合を検出します。

struct test_relacy_behaviour : public rl::test_suite<test_relacy_behaviour, 2>
{
    rl::var<std::string*> ptr;
    rl::var<int> data;

    void before()
    {
        ptr($) = nullptr;
        rl::atomic_thread_fence(rl::memory_order_seq_cst);
    }

    void thread(unsigned int id)
    {
        if (id == 0) {
            std::string* p  = new std::string("Hello");
            data($) = 42;
            rl::atomic_thread_fence(rl::memory_order_release);
            ptr($) = p;
        }
        else {
            std::string* p2 = ptr($);        // <-- Test fails here after the first thread completely finishes executing (no contention)
            rl::atomic_thread_fence(rl::memory_order_acquire);

            RL_ASSERT(!p2 || *p2 == "Hello" && data($) == 42);
        }
    }

    void after()
    {
        delete ptr($);
    }
};

Relacy の作成者に連絡して、これが予期された動作であるかどうかを確認しました。彼は、私のテストケースには確かにデータ競合があると言います。しかし、私はそれを見つけるのに苦労しています。誰かが私にレースが何であるかを指摘できますか? 最も重要なことは、これら 3 つのケースの違いは何ですか?

更新: Relacy は、スレッド間でアクセスされる変数のアトミック性(またはその欠如) について単に不平を言っている可能性があることに気づきました...結局のところ、このコードをプラットフォームでのみ使用するつもりであることを認識していません。ここで、整列された整数/ポインター アクセスは当然アトミックです。

別の更新: Jeff Preshing が、明示的なフェンスと組み込みのフェンス (「フェンス」と「操作」)の違いを説明する優れたブログ投稿を書きました。ケース 2 と 3 は明らかに同等ではありません。(とにかく、特定の微妙な状況では。)

4

5 に答える 5

14

コードには競争があると思います。ケース 1 とケース 2 は同等ではありません。

29.8 [atomics.fences]

-2- AXの前にシーケンスされ、XがMを変更し、YがBの前にシーケンスされ、 Yのように、アトミック オブジェクトMに対して動作するアトミック操作XおよびYが存在する場合、リリース フェンスAは取得フェンスBと同期します。 Xによって書き込まれた値、または仮想的なリリース シーケンスXがリリース操作の場合にヘッドになる副作用によって書き込まれた値を読み取ります。

ケース 1 では、リリース フェンスは取得フェンスと同期しません。これptrは、 がアトミック オブジェクトではなく、ストアとロードptrがアトミック操作ではないためです。

ケース 2 とケース 3 は同等です (実際には完全ではありません。LWimsey のコメントと回答を参照してください) ptr。([atomic.fences] の段落 3 と 4 は、フェンスがアトミック操作とどのように同期するか、またその逆について説明しています。)

フェンスのセマンティクスは、アトミック オブジェクトとアトミック操作に関してのみ定義されます。ターゲット プラットフォームと実装がより強力な保証 (任意のポインター型をアトミック オブジェクトとして扱うなど) を提供するかどうかは、せいぜい実装によって定義されます。

注意: ケース 2 とケース 3 の両方でptr、ストアの前に取得操作が発生する可能性があるため、初期化されていない からガベージが読み取られatomic<int*>ます。取得操作と解放操作 (またはフェンス) を使用するだけでは、ロードの前にストアが行われることは保証されません。ロードが格納された値を読み取る場合に、コードが正しく同期されることが保証されるだけです。

于 2013-01-06T14:26:03.757 に答える
14

関連する参考文献:

上記のいくつかは、あなたや他の読者の興味を引くかもしれません。

于 2013-01-06T18:25:53.547 に答える
1

アトミック変数をサポートするメモリは、アトミックの内容にのみ使用できます。ただし、ケース 1 の ptr のような単純な変数は別の話です。コンパイラが書き込み権限を取得すると、レジスタが不足した場合の一時的な値であっても、何でも書き込むことができます。

あなたの例は病的にきれいです。もう少し複雑な例を考えると:

std::string* p  = new std::string("Hello");
data($) = 42;
rl::atomic_thread_fence(rl::memory_order_release);
std::string* p2 = new std::string("Bye");
ptr($) = p;

コンパイラがポインタの再利用を選択することは完全に合法です

std::string* p  = new std::string("Hello");
data($) = 42;
rl::atomic_thread_fence(rl::memory_order_release);
ptr($) = new std::string("Bye");
std::string* p2 = ptr($);
ptr($) = p;

なぜそうするのでしょうか?わかりませんが、おそらくキャッシュラインなどを保持するためのエキゾチックなトリックです。ポイントは、ケース 1 では ptr がアトミックではないため、行 'ptr($) = p' への書き込みと 'std::string* p2 = ptr($)' への読み取りの間に競合ケースがあることです。未定義の動作をもたらします。この単純なテストケースでは、コンパイラはこの権利を行使することを選択しない可能性があり、それは安全かもしれませんが、より複雑なケースでは、コンパイラは ptr を好きなように悪用する権利を持ち、Relacy はこれをキャッチします。

このトピックに関する私のお気に入りの記事: http://software.intel.com/en-us/blogs/2013/01/06/benign-data-races-what-c​​ould-possibly-go-wrong

于 2013-09-03T05:18:57.797 に答える