41

私は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();
}
4

2 に答える 2

44

あなたの使用法はあなたがあなたのコメントで言及することを実際に保証するものではありません。つまり、フェンスを使用しても、への割り当て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<<aa=11

于 2012-11-29T19:38:03.523 に答える
7

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

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

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

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

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

于 2012-11-29T18:43:12.720 に答える