原子操作怎么可能不是同步操作呢?
How can an atomic operation be not a synchronization operation?
标准说宽松的原子操作不是同步操作。但是对于其他线程看不到其操作结果的原子性是什么。
示例 here 不会给出预期的结果,对吗?
我对同步的理解是所有线程都可以看到具有这种特征的操作的结果。
可能我不明白同步是什么意思。
我的逻辑漏洞在哪里?
是的,标准是正确的。宽松原子不是同步操作,因为只保证操作的原子性。
例如,
int k = 5;
void foo() {
k = 10;
}
int baz() {
return k;
}
存在多个线程时,行为未定义,因为它暴露了竞争条件。在某些架构的实践中,可能会发生 baz
的调用者不会看到 10,也不会看到 5,而是其他一些不确定的值。它通常被称为 torn 或 dirty read.
如果改用宽松的原子加载和存储,baz
将保证 return 5 或 10,因为不会出现数据竞争。
值得注意的是,出于实用目的,Intel 芯片及其非常强大的内存模型在这种通用架构上使 relaxed atomic 成为一个 noop(意味着它是原子的,没有额外的成本),因为加载和存储是原子的在硬件层面。
假设我们有
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()
和 bar()
访问相同的非原子变量,可能会发生数据竞争。
现在假设我们将示例更改为:
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();
仍然不能保证线程2观察到值1;毕竟,加载可能发生在存储之前。但是,在这种情况下,if 线程 2 观察到值 1,然后线程 1 中的存储与线程 2 中的加载同步。这意味着在存储之前排序的所有内容线程 1 发生在线程 2 加载之后排序的所有内容之前。因此,bar()
将看到 foo()
产生的所有副作用,并且如果它们都访问相同的非原子变量,则不会发生数据竞争发生。
因此,如您所见,x
上操作的同步属性不会告诉您 x
发生了什么。相反,同步对两个线程中的 surrounding 操作强加了顺序。 (因此,在链接示例中,结果始终为 5,并且不依赖于内存排序;取加操作的同步属性不影响取加操作本身的效果。)
允许编译器和 CPU 重新排序内存访问。它是 as-if rule,它假设一个单线程进程。
在多线程程序中,内存顺序参数指定如何围绕原子操作对内存访问进行排序。这是与原子性方面本身分开的原子操作的同步方面("acquire-release semantics"):
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
使多线程程序很难推理,并且应该仅在非常特殊的情况下使用,当它们之间没有依赖关系时原子操作和任何线程中它之前或之后的任何其他操作(这种情况很少见)。
标准说宽松的原子操作不是同步操作。但是对于其他线程看不到其操作结果的原子性是什么。
示例 here 不会给出预期的结果,对吗?
我对同步的理解是所有线程都可以看到具有这种特征的操作的结果。
可能我不明白同步是什么意思。 我的逻辑漏洞在哪里?
是的,标准是正确的。宽松原子不是同步操作,因为只保证操作的原子性。
例如,
int k = 5;
void foo() {
k = 10;
}
int baz() {
return k;
}
存在多个线程时,行为未定义,因为它暴露了竞争条件。在某些架构的实践中,可能会发生 baz
的调用者不会看到 10,也不会看到 5,而是其他一些不确定的值。它通常被称为 torn 或 dirty read.
如果改用宽松的原子加载和存储,baz
将保证 return 5 或 10,因为不会出现数据竞争。
值得注意的是,出于实用目的,Intel 芯片及其非常强大的内存模型在这种通用架构上使 relaxed atomic 成为一个 noop(意味着它是原子的,没有额外的成本),因为加载和存储是原子的在硬件层面。
假设我们有
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()
和 bar()
访问相同的非原子变量,可能会发生数据竞争。
现在假设我们将示例更改为:
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();
仍然不能保证线程2观察到值1;毕竟,加载可能发生在存储之前。但是,在这种情况下,if 线程 2 观察到值 1,然后线程 1 中的存储与线程 2 中的加载同步。这意味着在存储之前排序的所有内容线程 1 发生在线程 2 加载之后排序的所有内容之前。因此,bar()
将看到 foo()
产生的所有副作用,并且如果它们都访问相同的非原子变量,则不会发生数据竞争发生。
因此,如您所见,x
上操作的同步属性不会告诉您 x
发生了什么。相反,同步对两个线程中的 surrounding 操作强加了顺序。 (因此,在链接示例中,结果始终为 5,并且不依赖于内存排序;取加操作的同步属性不影响取加操作本身的效果。)
允许编译器和 CPU 重新排序内存访问。它是 as-if rule,它假设一个单线程进程。
在多线程程序中,内存顺序参数指定如何围绕原子操作对内存访问进行排序。这是与原子性方面本身分开的原子操作的同步方面("acquire-release semantics"):
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
使多线程程序很难推理,并且应该仅在非常特殊的情况下使用,当它们之间没有依赖关系时原子操作和任何线程中它之前或之后的任何其他操作(这种情况很少见)。