使用显式fences和std :: atomic有什么区别?

Cam*_*ron 25 c++ atomic memory-fences c++11

假设对齐的指针加载和存储在目标平台上自然是原子的,这有什么区别:

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

这个:

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

还有这个:

// 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);
Run Code Online (Sandbox Code Playgroud)

我的印象是它们都是等价的,但是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($);
    }
};
Run Code Online (Sandbox Code Playgroud)

我联系了Relacy的作者,看看这是否是预期的行为; 他说在我的测试用例中确实存在数据竞争.但是,我发现它时遇到了麻烦; 谁能指出我的比赛是什么?最重要的是,这三种情况之间有什么区别?

更新:我发现Relacy可能只是在抱怨跨线程访问的变量的原子性(或缺乏,而不是)...毕竟,它不知道我打算只在平台上使用这个代码对齐的整数/指针访问自然是原子的.

另一个更新:Jeff Preshing撰写了一篇精彩的博客文章,解释了显式围栏和内置围栏("围栏"与"操作")之间的区别.案例2和3显然不相同!(无论如何,在某些微妙的情况下.)

Jon*_*ely 13

我相信代码有竞争.案例1和案例2不相同.

29.8 [atomics.fences]

如果存在原子操作XY,则释放围栏A与获取围栏B同步,两者都在某个原子对象M上操作,这样AX之前排序,X修改M,YB之前排序,Y读取由X写入的值或由假设释放序列中的任何副作用写入的值,如果它是释放操作,则X将结束.

在案例1中,您的版本围栏不与您的获取围栏同步,因为ptr它不是原子对象,并且存储和加载ptr不是原子操作.

案例2和案例3是等价的(实际上,并不完全,请参阅LWimsey的评论和答案),因为它ptr是一个原子对象,而商店是一个原子操作.([atomic.fences]的第3和第4段描述了栅栏如何与原子操作同步,反之亦然.)

围栏的语义仅针对原子对象和原子操作进行定义.您的目标平台和您的实现是否提供更强的保证(例如将任何指针类型视为原子对象)是最佳实现定义.

对于案例2和案例3的NB,获取操作ptr可能发生在商店之前,因此将从未初始化的读取垃圾atomic<int*>.简单地使用获取和释放操作(或围栏)并不能确保存储在加载之前发生,它只能确保如果加载读取存储的值,那么代码就会正确同步.

  • 并非所有平台都有此类说明.在一个平台上,C++围栏可能映射到那些指令并且您的代码可能有效,但标准是用更抽象的术语定义的.C++ fences可用于将同步添加到几个放松原子操作的序列中,例如,您可以对五个不同的原子对象执行五个放松的存储,并且仅使用一个释放栅栏,并且执行五个放松的加载并且仅具有一个获取栅栏.这可能比五个seqcst商店和五个seqcst负载便宜.在你的代码中,使用单个原子对象,我只使用`atomic <string*>` (5认同)
  • @JonathanWakely我怀疑情况2和情况3是否相等。情况2似乎是正确的,但在情况3中,在释放防护之后对整数内存(“ new”)的分配进行了排序,这意味着即使另一个线程在加载“ ptr”之后正确地发出了获取防护,它仍可能指向由于该内存分配未正确同步,因此处于垃圾状态。 (2认同)

thb*_*thb 12

几个相关的参考文献:

上述部分内容可能会引起您和其他读者的兴趣.


LWi*_*sey 5

尽管各种答案涵盖了潜在问题的点点滴滴和/或提供了有用的信息,但没有任何答案正确地描述了这三种情况下的潜在问题。

为了在线程之间同步内存操作,使用释放和获取屏障来指定顺序。
在该图中,线程1中的内存操作A无法在(单向)释放屏障上向下移动(无论这是对原子存储区的释放操作,还是在紧随其后的原子存储区之后的独立释放防护)。因此,保证了内存操作A会在原子存储之前发生。线程2中的内存操作B不能通过获取屏障向上移动,也是如此。因此原子加载发生在内存操作B之前。

在此处输入图片说明

原子ptr本身根据对单个修改顺序的保证来提供线程间排序。只要线程2看到的值ptr,就可以确保存储(以及内存操作A)在加载之前发生。因为保证了负载发生在内存操作B之前,所以传递性规则说内存操作A发生在B且同步完成之前。

这样,让我们​​看一下您的3种情况。

情况1损坏是因为ptr非原子类型在不同线程中进行了修改。这是数据争用的经典示例,它会导致未定义的行为。

情况2是正确的。作为参数,带有的整数分配new在释放操作之前进行排序。这等效于:

// Case 2: atomic var, automatic fence
std::atomic<int*> ptr;
// ...
int *tmp = new int(-4);
ptr.store(tmp, std::memory_order_release);
Run Code Online (Sandbox Code Playgroud)

情况3已被破坏,尽管以一种微妙的方式。问题是,即使ptr分配在独立的篱笆后面正确排序,整数分配(new)也会在篱笆后面排序,从而导致整数内存位置上的数据争用。

该代码等效于:

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

int *tmp = new int(-4);
ptr.store(tmp, std::memory_order_relaxed);
Run Code Online (Sandbox Code Playgroud)

如果将其映射到上图,则该new操作符应该是内存操作A的一部分。由于在释放范围下方进行了排序,因此可以保证排序不再有效,并且整数分配实际上可以通过线程2中的内存操作B重新排序。 ,load()线程2中的a可能返回垃圾或导致其他未定义的行为。

  • @Igor由于整数分配是在释放栅栏之后排序的,因此此操作和“ptr.store(mo_relaxed)”之间没有线程间排序。分配在分配给“ptr”之前完成(确实发生在之前),但这保证只在同一线程内保存。如果没有释放栅栏强制执行的顺序(除其他效果外),线程 B 可能会观察到这些操作无序(技术上未定义的行为)。 (2认同)
  • @Igor 对于“操作”,我的意思是“int *tmp = ...”和“ptr.store(tmp, mo_relaxed)”。你的假设是不正确的;一旦线程 B 观察到 `ptr` 的新值,它可能仍然在该内存位置(未初始化)处看不到 `-4`,或者内存位置本身甚至可能无效。这就是未定义行为的问题,你无法真正推理它;但如果 `tmp = new ...` 在栅栏之前排序,那么一切都是明确定义的。 (2认同)
  • @LWimsey 很好的答案和出色的工作,指出此时只有地址已被修复,所有其他分配工作可能尚未完成(并且不能保证在另一个线程中可见。正如您也解释的那样)。我强烈建议 C++ 程序员永远不要假设标准未“明确”强制要求的“任何”同步类型。 (2认同)