假设对齐指针加载和存储在目标平台上自然原子,也正是这一区别:
// 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笔者发现如果这是预期的行为; 他说,的确有我的测试情况下的数据竞争。 然而,我无法察觉它; 能有人指出我的种族是什么? 最重要的,什么是这三种情况之间的差异?
更新 :它发生,我认为Relacy可以简单地抱怨原子 (或缺乏,相当)的跨线程访问的变量...毕竟,它不知道,我只打算使用的平台上的代码其中对准整数/指针访问是自然原子。
另一个更新 :杰夫Preshing写了一本优秀的博客文章解释明确围栏和内置者之间的区别 (“围栏”与“行动”)。 例2和3是显然并不等同! (在某些情况微妙,反正。)
Answer 1:
我相信代码有一个比赛。 案例1和2的情况下是不等价的。
29.8 [atomics.fences]
-2-甲释放围栏阿与获取围栏乙如果存在的原子操作X和Y同步,在某些原子物体M,使得A是X之前测序两个操作,X修改M,Y是B之前测序,和Y读取由X或通过任何副作用在假想释放序列X写入好像它是一个释放操作将朝向一个值写入的值。
在案例1中,因为你释放围栏不与你的获取栅栏同步ptr
不是一个原子对象以及存储和加载在ptr
不是原子操作。
案例2和3的情况下是等价的( 实际上,不完全,看到LWimsey的意见,回答 ),因为ptr
是一个原子对象和实体店是一个原子操作。 (第3和的[atomic.fences] 4描述了一个围栏与一个原子操作如何同步,反之亦然。)
围栏的语义仅相对于原子对象和原子操作定义。 无论您的目标平台和您的实现提供了强大的保证(如处理任何指针类型作为一个原子对象)是实现定义的最好的。
NB两个壳体2和壳体3上的获取操作的ptr
可以在存储之前发生,因此将从未初始化读垃圾atomic<int*>
简单地使用获取和释放操作(或栅栏)不保证存储负载之前发生,它只确保当负载读取所存储的值,则该代码被正确地同步。
Answer 2:
一些相关的参考资料:
- 在C ++ 11标准草案 (PDF,见第1,29和30);
- 汉斯-J。 在C ++并行的贝姆的概况 ;
- 麦肯尼,Boehm和Crowl上并发在C ++ ;
- GCC对并发性发育笔记C ++ ;
- Linux内核上并行笔记 ;
- 在这里#2回答一个相关的问题 ;
- 与答案另一个相关的问题 ;
- Cppmem,其中实验并发沙箱;
- Cppmem的帮助页面 ;
- 自旋,用于分析并发系统的逻辑一致性的工具;
- 的从硬件角度存储器障碍的概述 (PDF)。
上述的一些您可能感兴趣的和其他读者。
Answer 3:
虽然各种各样的回答涵盖的潜在问题是什么和/或提供有用的信息的点点滴滴,没有回答正确地描述了这三种情况下的潜在问题。
为了线程之间的内存操作同步,发布和获取障碍用于指定排序。
在该图中,在线程1的存储器操作A不能跨越(单程)释放屏障(无论是否是在原子商店的释放操作,或一个独立的释放围栏随后松弛原子商店)向下移动。 因此,存储操作的保证原子存储之前发生。 这同样适用于在螺纹2不能穿过屏障获取向上移动存储器操作B; 因此原子负载之前的存储器操作B.发生
原子ptr
本身提供了一个基于它有一个单独的修改为了保证线程间的排序。 一旦线程2看到了一个值ptr
,它保证了存储(并且因此存储器操作A)负载之前发生。 由于负载保证之前的内存操作B到发生,传递的规则说,存储操作的发生之前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);
案例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);
如果映射是上面的图中, new
运营商应该是正在测序的释放栅栏下方的内存操作A.一部分,排序保证不再持有和整数分配实际上可能与内存操作的B线2被重新排序。因此,一个load()
在线程2可以返回垃圾或导致其他未定义的行为。
Answer 4:
存储器备份的原子变量永远只能被用于原子的内容。 然而,一个普通的可变,如同壳体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;
为什么会这么做? 我不知道,也许是一些外来伎俩,以保持高速缓存行或东西。 点是,由于ptr不万一1个原子,有写入线“PTR($)= P”和所读取之间的竞争情况下“的std :: string * P2 = PTR($)”,产生不确定的行为。 在这个简单的测试情况下,编译器可能不会选择行使这一权利,并且它可能是安全的,但在更复杂的情况下,编译器有滥用权PTR但是它为所欲为,而Relacy抓住这一点。
我的话题最喜欢的文章: http://software.intel.com/en-us/blogs/2013/01/06/benign-data-races-what-could-possibly-go-wrong
Answer 5:
在第一个例子中的比赛是指针的出版,它指向的东西之间。 原因是,你有围栏(=在同一侧的指针出版) 之后的创建和指针的初始化:
int* ptr; //noop
std::atomic_thread_fence(std::memory_order_release); //fence between noop and interesting stuff
ptr = new int(-4); //object creation, initalization, and publication
如果我们假设CPU访问正确对齐的指针是原子 ,代码可以通过编写此更正为:
int* ptr; //noop
int* newPtr = new int(-4); //object creation & initalization
std::atomic_thread_fence(std::memory_order_release); //fence between initialization and publication
ptr = newPtr; //publication
需要注意的是,尽管这可能工作在很多机器上正常,也绝对在C ++在最后一行的原子性标准之内没有保证。 所以最好使用atomic<>
在首位的变量。
文章来源: What is the difference between using explicit fences and std::atomic?