该标准说宽松的原子操作不是同步操作。但是其他线程看不到关于操作结果的原子性。
示例here不会给出预期的结果,对吧?
我通过同步了解到,具有这种特征的操作的结果将在所有线程中可见。
也许我不明白同步的含义。 我的逻辑上的漏洞在哪里?
答案 0 :(得分:2)
允许编译器和CPU重新排序存储器访问。它是as-if rule,它假设一个单线程进程。
在多线程程序中,内存顺序参数指定如何在原子操作周围 排序内存访问。这是与原子性方面本身分开的原子操作的同步方面(“获取-释放语义”):
int x = 1;
std::atomic<int> y = 1;
// Thread 1
x++;
y.fetch_add(1, std::memory_order_release);
// Thread 2
while ((y.load(std::memory_order_acquire) == 1)
{ /* wait */ }
std::cout << x << std::endl; // x is 2 now
在宽松的内存顺序的情况下,我们仅获得原子性,而没有顺序:
int x = 1;
std::atomic<int> y = 1;
// Thread 1
x++;
y.fetch_add(1, std::memory_order_relaxed);
// Thread 2
while ((y.load(std::memory_order_relaxed) == 1)
{ /* wait */ }
std::cout << x << std::endl; // x can be 1 or 2, we don't know
实际上,正如Herb Sutter在其出色的Atomic Weapons演讲中所解释的那样,memory_order_relaxed
使得多线程程序非常难以推理,并且仅应在非常具体的情况下使用,即原子之间不存在依赖性。操作和其他任何操作在任何线程之前或之后(很少)。
答案 1 :(得分:0)
是的,标准是正确的。松弛原子不是同步操作,因为只能保证操作的原子性。
例如
int k = 5;
void foo() {
k = 10;
}
int baz() {
return k;
}
在有多个线程的情况下,此行为未定义,因为它公开了竞争条件。实际上,在某些体系结构上,baz
的调用者可能看不到10,不是5,而是其他不确定的值。通常被称为 torn 或 dirty 阅读。
如果改用宽松的原子加载和存储,baz
将确保返回5或10,因为不会发生数据争用。
值得注意的是,出于实用目的,英特尔芯片及其强大的内存模型使轻松的原子成为这种通用架构上的基础(意味着没有额外的成本,因为原子和负载在硬件上是原子的)级别。
答案 2 :(得分:0)
假设我们有
std::atomic<int> x = 0;
// thread 1
foo();
x.store(1, std::memory_order_relaxed);
// thread 2
assert(x.load(std::memory_order_relaxed) == 1);
bar();
首先,不能保证线程2将观察到值1(即断言可能会触发)。但是即使线程2确实观察到值1,而线程2正在执行bar()
时,它也可能不会观察到线程1中foo()
产生的副作用,并且如果foo()
和{{1 }}访问相同的非原子变量,可能会发生数据争用。
现在假设我们将示例更改为:
bar()
仍然不能保证线程2观察到值1;毕竟,加载可能发生在存储之前。但是,在这种情况下,如果 if 线程2观察到值1,则线程1中的存储将与线程2中的负载进行同步。这意味着在线程1中的存储之前发生的所有排序都将发生。在线程2中的加载之后所有要排序的事物之前。因此,std::atomic<int> x = 0;
// thread 1
foo();
x.store(1, std::memory_order_release);
// thread 2
assert(x.load(std::memory_order_acquire) == 1);
bar();
将看到bar()
产生的所有副作用,如果它们都访问相同的非原子变量,则不会发生数据争用。 / p>
因此,如您所见,foo()
上的操作的同步属性不会告诉您x
会发生什么情况。相反,同步将两个线程中的 surrounding 操作强加了顺序。 (因此,在链接的示例中,结果始终为5,并且不依赖于内存顺序; fetch-add操作的同步属性不会影响fetch-add操作本身的效果。)