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]
如果存在原子操作X和Y,则释放围栏A与获取围栏B同步,两者都在某个原子对象M上操作,这样A在X之前排序,X修改M,Y在B之前排序,Y读取由X写入的值或由假设释放序列中的任何副作用写入的值,如果它是释放操作,则X将结束.
在案例1中,您的版本围栏不与您的获取围栏同步,因为ptr它不是原子对象,并且存储和加载ptr不是原子操作.
案例2和案例3是等价的(实际上,并不完全,请参阅LWimsey的评论和答案),因为它ptr是一个原子对象,而商店是一个原子操作.([atomic.fences]的第3和第4段描述了栅栏如何与原子操作同步,反之亦然.)
围栏的语义仅针对原子对象和原子操作进行定义.您的目标平台和您的实现是否提供更强的保证(例如将任何指针类型视为原子对象)是最佳实现定义.
对于案例2和案例3的NB,获取操作ptr可能发生在商店之前,因此将从未初始化的读取垃圾atomic<int*>.简单地使用获取和释放操作(或围栏)并不能确保存储在加载之前发生,它只能确保如果加载读取存储的值,那么代码就会正确同步.
thb*_*thb 12
几个相关的参考文献:
上述部分内容可能会引起您和其他读者的兴趣.
尽管各种答案涵盖了潜在问题的点点滴滴和/或提供了有用的信息,但没有任何答案正确地描述了这三种情况下的潜在问题。
为了在线程之间同步内存操作,使用释放和获取屏障来指定顺序。
在该图中,线程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可能返回垃圾或导致其他未定义的行为。
| 归档时间: |
|
| 查看次数: |
7290 次 |
| 最近记录: |