int main() {
std::vector<int> foo;
std::atomic<int> bar{0};
std::mutex mx;
auto job = [&] {
int asdf = bar.load();
// std::lock_guard lg(mx);
foo.emplace_back(1);
bar.store(foo.size());
};
std::thread t1(job);
std::thread t2(job);
t1.join();
t2.join();
}
显然,这不能保证能正常工作,但可以与互斥锁一起工作。但是,如何用标准的正式定义来解释呢?
考虑以下摘自cppreference的内容:
如果线程A中的原子存储被标记为memory_order_release,并且线程B中来自同一变量的原子加载被标记为memory_order_acquire [与默认原子相同),则所有内存写操作(非原子和宽松原子)均发生在内存之前。从线程A的角度来看,原子存储在线程B中成为可见的副作用。也就是说,一旦原子加载完成,线程B就可以保证看到线程A写入内存的所有内容。
原子装载和存储(具有默认值或指定的特定获取和释放内存顺序)具有上述的获取-释放语义。(互斥锁的锁定和解锁也是如此。)
这种说法的解释可能是,当线程2的加载操作与线程1的存储操作同步时,可以保证观察到所有在存储之前发生的(甚至是非原子的)写操作,例如矢量修改,使得定义明确的。但是几乎每个人都会同意这会导致分段错误,并且如果job函数循环运行其三行,则肯定会这样做。
鉴于标准措辞似乎暗示原子将以某种方式同步,因此什么标准措辞解释了这两种工具之间功能上的明显差异。
我知道何时使用互斥体和原子,并且该示例不起作用,因为实际上没有同步发生。我的问题是如何解释该定义,以使其与现实中的工作方式不矛盾。
引用的段落意思是,当B加载A存储的值时,通过观察存储是否发生,还可以确保B在存储之前所做的所有操作也已发生并且可见。
但这并不能告诉您商店实际上尚未发生的任何事情!
我同意,如果线程B中的负载返回1,则可以安全地得出结论,另一个线程已完成存储,因此退出了关键部分,因此B可以安全地使用foo
。但是,如果两个线程在任何一个存储之前都执行了加载,则两个加载都可能返回0。您的代码甚至不查看加载的值,因此在这种情况下,两个线程可能会一起进入关键部分。
以下代码将是使用原子保护关键部分的一种安全但效率低的方法。它确保A将首先执行关键部分,B将等到A完成后再继续。(显然,如果两个线程都在等待另一个,则您将陷入死锁。)
int main() {
std::vector<int> foo;
std::atomic<int> bar{0};
std::mutex mx;
auto jobA = [&] {
foo.emplace_back(1);
bar.store(foo.size());
};
auto jobB = [&] {
while (bar.load() == 0) /* spin */ ;
foo.emplace_back(1);
};
std::thread t1(jobA);
std::thread t2(jobB);
t1.join();
t2.join();
}
本文收集自互联网,转载请注明来源。
如有侵权,请联系[email protected] 删除。
我来说两句