web-dev-qa-db-ja.com

C ++ 11メモリフェンスについて

私はc ++ 11のメモリフェンスを理解しようとしています、これを行うにはより良い方法、アトミック変数などがあることを知っていますが、この使い方が正しいかどうか疑問に思いました。このプログラムは何も役に立たないことに気づきました。フェンス関数の使用が、私が思ったとおりに機能することを確認したかっただけです。

基本的に、このリリースでは、フェンスの前にこのスレッドで行われた変更がフェンスの後の他のスレッドに表示され、2番目のスレッドでは、変数の変更がフェンスの直後のスレッドに表示されます。

私の理解は正しいですか?それとも完全にポイントを逃したのですか?

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

int a;

void func1()
{
    for(int i = 0; i < 1000000; ++i)
    {
        a = i;
        // Ensure that changes to a to this point are visible to other threads
        atomic_thread_fence(std::memory_order_release);
    }
}

void func2()
{
    for(int i = 0; i < 1000000; ++i)
    {
        // Ensure that this thread's view of a is up to date
        atomic_thread_fence(std::memory_order_acquire);
        std::cout << a;
    }
}

int main()
{
    std::thread t1 (func1);
    std::thread t2 (func2);

    t1.join(); t2.join();
}
37
jcoder

あなたの用法はnotで実際にあなたがコメントで言及していることを確実にします。つまり、フェンスを使用しても、aへの割り当てが他のスレッドから見えること、またはaから読み取った値が「最新」であることは保証されません。これは、フェンスをどこで使用するかという基本的な考え方があるように見えますが、実際にはコードがこれらのフェンスが「同期」するための正確な要件を満たしていないためです。

これは、正しい使用法をよりよく示すと思う別の例です。

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

std::atomic<bool> flag(false);
int a;

void func1()
{
    a = 100;
    atomic_thread_fence(std::memory_order_release);
    flag.store(true, std::memory_order_relaxed);
}

void func2()
{
    while(!flag.load(std::memory_order_relaxed))
        ;

    atomic_thread_fence(std::memory_order_acquire);
    std::cout << a << '\n'; // guaranteed to print 100
}

int main()
{
    std::thread t1 (func1);
    std::thread t2 (func2);

    t1.join(); t2.join();
}
_

アトミックフラグのロードとストアは、どちらもリラックスしたメモリ順序を使用するため、同期しません。フェンスがなければ、このコードはデータ競合になります。これは、異なるスレッドで非アトミックオブジェクトの競合する操作を実行しているためです。フェンスと同期がなければ、aで競合する操作の前に発生する関係はありません。 。

ただし、フェンスを使用すると、スレッド2がスレッド1によって書き込まれたフラグを読み取ることが保証されているため(その値が表示されるまでループするため)、リリースフェンスの後にアトミック書き込みが発生し、アトミック読み取りが発生したため、フェンスが同期されます。 -フェンスを取得する前に、フェンスが同期します。 (特定の要件については、§29.8/2を参照してください。)

この同期は、リリースフェンスが発生する前に、発生する前に、取得フェンスの後に発生することを意味します。したがって、aへの非アトミックな書き込みは、aの非アトミックな読み取りの前に発生します。

ループ内で変数を書き込んでいるときは、特定の反復では発生前の関係を確立し、他の反復では確立しない可能性があるため、事態はより複雑になり、データ競合が発生します。

_std::atomic<int> f(0);
int a;

void func1()
{
    for (int i = 0; i<1000000; ++i) {
        a = i;
        atomic_thread_fence(std::memory_order_release);
        f.store(i, std::memory_order_relaxed);
    }
}

void func2()
{
    int prev_value = 0;
    while (prev_value < 1000000) {
        while (true) {
            int new_val = f.load(std::memory_order_relaxed);
            if (prev_val < new_val) {
                prev_val = new_val;
                break;
            }
        }

        atomic_thread_fence(std::memory_order_acquire);
        std::cout << a << '\n';
    }
}
_

このコードは依然としてフェンスを同期させますが、データ競合を排除しません。たとえば、f.load()がたまたま10を返した場合、_a=1_、_a=2_、... _a=10_はすべて、特定の_cout<<a_より前に発生したことがわかります。 、しかし私たちはしないでください _cout<<a_が_a=11_の前に発生することを知っています。これらは異なるスレッドで競合する操作であり、前に発生する関係はありません。データ競争。

41
bames53

使い方は正しいですが、有用なものを保証するには不十分です。

たとえば、コンパイラは、以下のようにしたい場合、内部でこのようにa = i;を自由に実装できます。

 while(a != i)
 {
    ++a;
    atomic_thread_fence(std::memory_order_release);
 }

したがって、他のスレッドはすべての値を見る可能性があります。

もちろん、コンパイラーがそのような単純な割り当てを実装することは決してありません。ただし、同様に複雑な動作が実際に最適化される場合があるため、通常のコードが特定の方法で内部的に実装されていることに依存することは非常に悪い考えです。これが、アトミック操作やフェンスのようなものがあり、そのような操作で使用した場合にのみ保証された結果を生成する理由です。

7
David Schwartz