- html - 出于某种原因,IE8 对我的 Sass 文件中继承的 html5 CSS 不友好?
- JMeter 在响应断言中使用 span 标签的问题
- html - 在 :hover and :active? 上具有不同效果的 CSS 动画
- html - 相对于居中的 html 内容固定的 CSS 重复背景?
一般来说,对于 int num
, num++
(或 ++num
),作为读-修改-写操作,是 不是原子的 .但是我经常看到编译器,比如GCC ,为其生成以下代码( try here ):
void f()
{
int num = 0;
num++;
}
f():
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], 0
add DWORD PTR [rbp-4], 1
nop
pop rbp
ret
从第 5 行开始,对应于
num++
是一条指令,我们可以得出结论
num++
是原子的 在这种情况下?
num++
可以在并发(多线程)场景中使用,没有任何数据竞争的危险 (即我们不需要制作它,例如
std::atomic<int>
并强加相关成本,因为它无论如何都是原子的)?
lock
的开销。前缀。而且,正如在关于单处理器机器的部分中提到的公认答案,以及
this answer ,其评论中的对话和其他人解释,
可以 (虽然不是用 C 或 C++)。
最佳答案
这绝对是 C++ 定义的导致未定义行为的数据竞争,即使一个编译器碰巧生成了在某些目标机器上执行您希望的代码。您需要使用 std::atomic
以获得可靠的结果,但您可以将其与 memory_order_relaxed
一起使用如果你不关心重新排序。有关使用 fetch_add
的一些示例代码和 asm 输出,请参见下文。 .
但首先,问题的汇编语言部分:
Since num++ is one instruction (
add dword [num], 1
), can we conclude that num++ is atomic in this case?
add dword [num], 1
在一个循环中会踩到对方的商店。 (请参阅
@Margaret's answer 以获得漂亮的图表)。从两个线程中的每一个增加 40k 之后,计数器在真正的多核 x86 硬件上可能只增加了 ~60k(不是 80k)。
lock
prefix可以应用于许多读-修改-写(内存目标)指令,以使整个操作相对于系统中所有可能的观察者(其他内核和 DMA 设备,而不是连接到 CPU 引脚的示波器)具有原子性。这就是它存在的原因。 (另见
this Q&A)。
lock add dword [num], 1
是原子的 .运行该指令的 CPU 内核会将缓存行保持在其私有(private) L1 缓存中的修改状态,从加载从缓存中读取数据,直到存储将其结果提交回缓存。根据
MESI cache coherency protocol 的规则,这可以防止系统中的任何其他缓存在从加载到存储的任何时间点拥有缓存行的拷贝。 (或分别由多核 AMD/Intel CPU 使用的 MOESI/MESIF 版本)。因此,其他核心的操作似乎发生在之前或之后,而不是期间。
lock
前缀,另一个核心可以获取缓存行的所有权并在我们的加载之后但在我们的存储之前修改它,这样其他存储将在我们的加载和存储之间变得全局可见。其他几个答案都弄错了,并声称没有
lock
你会得到相同缓存行的冲突拷贝。这在具有一致缓存的系统中永远不会发生。
lock
ed 指令在跨越两个缓存行的内存上运行,则需要做更多的工作来确保对象的两个部分的更改在传播到所有观察者时保持原子性,因此没有观察者可以看到撕裂。 CPU 可能必须锁定整个内存总线,直到数据到达内存为止。不要错位原子变量!)
lock
前缀还将指令变成完整的内存屏障(如
MFENCE ),停止所有运行时重新排序,从而提供顺序一致性。 (参见
Jeff Preshing's excellent blog post 。他的其他帖子也都很出色,清楚地解释了很多关于
lock-free programming 的好东西,从 x86 和其他硬件细节到 C++ 规则。)
lock
前缀。其他代码访问共享变量的唯一方法是让 CPU 进行上下文切换,这不能在指令中间发生。所以一个简单的
dec dword [num]
可以在单线程程序及其信号处理程序之间同步,或者在单核机器上运行的多线程程序中同步。见
the second half of my answer on another question ,以及它下面的评论,我在那里更详细地解释了这一点。
num++
完全是假的无需告诉编译器您需要将其编译为单个读-修改-写实现:
;; Valid compiler output for num++
mov eax, [num]
inc eax
mov [num], eax
如果您使用
num
的值,这很有可能稍后:编译器将在增量后将其保存在寄存器中。所以即使你检查如何
num++
自行编译,更改周围的代码会影响它。
inc dword [num]
;现代 x86 CPU 将至少与使用三个独立指令一样有效地运行内存目标 RMW 指令。有趣的事实:
gcc -O3 -m32 -mtune=i586
will actually emit this,因为(奔腾)P5 的超标量管道没有像 P6 和更高版本的微体系结构那样将复杂的指令解码为多个简单的微操作。有关更多信息,请参阅
Agner Fog's instruction tables / microarchitecture guide,许多有用的链接(包括 Intel 的 x86 ISA 手册,请参阅
x86 标签维基)以 PDF 格式免费提供))。
num++
只有在其他一些操作之后才会全局可见。
flag.store(1, std::memory_order_release);
告诉编译器不要重新排序。 .
// int flag; is just a plain global, not std::atomic<int>.
flag--; // Pretend this is supposed to be some kind of locking attempt
modify_a_data_structure(&foo); // doesn't look at flag, and the compiler knows this. (Assume it can see the function def). Otherwise the usual don't-break-single-threaded-code rules come into play!
flag++;
但不会。编译器可以自由移动
flag++
跨函数调用(如果它内联函数或知道它不查看
flag
)。然后它可以完全优化掉修改,因为
flag
甚至不是
volatile
.
volatile
不是 std::atomic 的有用替代品。 std::atomic 确实让编译器假设内存中的值可以类似于
volatile
异步修改,但还有更多. (实际上有
similarities between volatile int to std::atomic with mo_relaxed 用于纯加载和纯存储操作,但不是用于 RMW。此外,
volatile std::atomic<int> foo
不一定与
std::atomic<int> foo
相同,尽管当前的编译器不优化原子(例如2 个相同值的背靠背存储),因此 volatile atomic 不会更改代码生成。)
lock
prefix是一个完整的内存屏障,所以使用
num.fetch_add(1, std::memory_order_relaxed);
在 x86 上生成与
num++
相同的代码(默认为顺序一致性),但在其他架构(如 ARM)上效率更高。即使在 x86 上,relaxed 也允许更多的编译时重新排序。
std::atomic
上运行的函数全局变量。
#include <atomic>
std::atomic<int> num;
void inc_relaxed() {
num.fetch_add(1, std::memory_order_relaxed);
}
int load_num() { return num; } // Even seq_cst loads are free on x86
void store_num(int val){ num = val; }
void store_num_release(int val){
num.store(val, std::memory_order_release);
}
// Can the compiler collapse multiple atomic operations into one? No, it can't.
# g++ 6.2 -O3, targeting x86-64 System V calling convention. (First argument in edi/rdi)
inc_relaxed():
lock add DWORD PTR num[rip], 1 #### Even relaxed RMWs need a lock. There's no way to request just a single-instruction RMW with no lock, for synchronizing between a program and signal handler for example. :/ There is atomic_signal_fence for ordering, but nothing for RMW.
ret
inc_seq_cst():
lock add DWORD PTR num[rip], 1
ret
load_num():
mov eax, DWORD PTR num[rip]
ret
store_num(int):
mov DWORD PTR num[rip], edi
mfence ##### seq_cst stores need an mfence
ret
store_num_release(int):
mov DWORD PTR num[rip], edi
ret ##### Release and weaker doesn't.
store_num_relaxed(int):
mov DWORD PTR num[rip], edi
ret
请注意在顺序一致性存储之后如何需要 MFENCE(一个完整的屏障)。 x86 通常是强排序的,但允许 StoreLoad 重新排序。拥有存储缓冲区对于流水线乱序 CPU 的良好性能至关重要。 Jeff Preshing 的
Memory Reordering Caught in the Act 展示了不使用 MFENCE 的后果,用真实的代码来展示在真实硬件上发生的重新排序。
num++; num-=2;
操作合二为一 num--;
说明 :
fetch_or(0)
之类的东西。这可能会变成一个
load()
(但仍然具有获取和释放语义),即使原始源没有任何明显冗余的原子操作。
std::shared_ptr<T>
避免冗余原子操作并不总是那么容易,不过,因为没有它的非原子版本(尽管
one of the answers here 提供了一种简单的方法来为 gcc 定义
shared_ptr_unsynchronized<T>
)。
num++; num-=2;
像编译一样编译
num--
:
num
是
volatile std::atomic<int>
.如果可以进行重新排序,则 as-if 规则允许编译器在编译时决定它总是以这种方式发生。没有什么可以保证观察者可以看到中间值(
num++
结果)。
lock dec dword [num]
而不是
lock inc dword [num]
/
lock sub dword [num], 2
.
num++; num--
不能消失,因为它与查看
num
的其他线程仍有同步关系。 ,并且它既是获取加载又是释放存储,不允许在该线程中重新排序其他操作。对于 x86,这可能可以编译为 MFENCE,而不是
lock add dword [num], 0
(即
num += 0
)。
shared_ptr
的拷贝被创建和销毁时,ref 计数,如果编译器可以证明另一个
shared_ptr
对象存在于临时对象的整个生命周期。)
num++; num--
当一个线程立即解锁和重新锁定时,合并可能会损害锁定实现的公平性。如果它从未在 asm 中真正释放过,那么即使是硬件仲裁机制也不会给另一个线程在此时获取锁的机会。
lock
ed 操作,即使使用
memory_order_relaxed
在最明显可优化的情况下。 (
Godbolt compiler explorer 以便您可以查看最新版本是否不同。)
void multiple_ops_relaxed(std::atomic<unsigned int>& num) {
num.fetch_add( 1, std::memory_order_relaxed);
num.fetch_add(-1, std::memory_order_relaxed);
num.fetch_add( 6, std::memory_order_relaxed);
num.fetch_add(-5, std::memory_order_relaxed);
//num.fetch_add(-1, std::memory_order_relaxed);
}
multiple_ops_relaxed(std::atomic<unsigned int>&):
lock add DWORD PTR [rdi], 1
lock sub DWORD PTR [rdi], 1
lock add DWORD PTR [rdi], 6
lock sub DWORD PTR [rdi], 5
ret
关于c++ - num++ 可以是 'int num' 的原子吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/39393850/
有没有办法用连词创建原子 if ?也就是说,我可以以某种方式在 C 中自动测试 if(A && B) 吗?如果它在第一个连接处短路,那么没问题,但如果没有短路,则在检查 B 时,A 可能已更改。有什么
我有很多 fork 的过程。子进程做很多事情和另一个系统调用。 当任何子进程从系统调用中获取错误时,它会将错误描述打印到 stderr 并将 SIGUSR1 发送到组长(主要父进程)。 SIGUSR1
阅读 boost::atomic 上的文档和 std::atomic 让我感到困惑的是 atomic 是否接口(interface)应该支持非平凡类型? 也就是说,给定一个只能通过将读/写包含在一个完
我有一个命令,可以将叠加图像放在视频上。 之后,我调整输出大小以适合某些尺寸。 通常一切正常,但有时且仅在某台台式计算机上,当第二次精化开始时,命令返回错误:moov atom not found 让
我最近发现当 LANG 设置为 C.utf8 时,X11 原子 WM_NAME 未在 Swing JFrame 中设置。但为 LANG 的其他值设置。这发生在带有 OpenJDK 11.0.9 的 L
我目前正在使用blackmagic的prorecorder录制视频。我使用 ffmpeg 将视频即时转码为 mp4 视频容器。持续时间未知,因为我正在对 prorecorder 输出到命名管道的 .t
这里真的有人使用 atom 来处理 git 提交消息吗?我想但我遇到了这个问题并且一直坚持使用 git commit -m '....' 。当我尝试使用 atom 时,它会打开 atom,我几乎立即从
考虑: void foo() { std::vector> foo(10); ... } foo 的内容现在有效吗?或者我是否需要显式循环并初始化它们?我检查过 Godbolt,看起来不错,但
在official FAQ我阅读的 Memcached: “发送到 memcached 的所有单独命令都是绝对原子的。” 然而,当涉及到 get_multi 和 set_multi 时,我仍然不清楚。
在测试程序的可扩展性时,我遇到了必须将 memcpy 操作设置为原子操作的情况。我必须将 64 字节的数据从一个位置复制到另一个位置。 我遇到了一种解决方案,即使用旋转变量: struct recor
我对 C++ 原子变量感到困惑。如果我有一个原子 x,我想在一个线程中递增并在另一个线程中读取,我可以执行++x 还是必须执行 x.atomic_fetch_add(1)。在读者线程中,我可以做类似
跟进自 Multiple assignment in one line ,我很想知道这对原子数据类型是如何工作的,特别是 bool 类型的例子。 给定: class foo { std::at
我想创建一个版本控制系统,并且对版本号为 1 的新条目的查询如下所示: ID 和修订号组合起来就是主键。 insert into contentfile (id, name, revision, ac
我在 iOS 项目中有下一个独立的测试片段: /// ... std::atomic_bool ab; ab.store(true); bool expected = false; while (!a
我了解如何使用条件变量(此构造的名称很糟糕,IMO,因为 cv 对象既不是变量也不表示条件)。所以我有一对线程,canonically使用 Boost.Thread 设置为: bool awake =
因此,对于最终项目,我尝试制作一款包含三种不同 meteor 的游戏;铜牌、银牌和金牌。虽然青铜阵列在Setup()中工作正常,但银色和金色 meteor 由于某种未知原因而高速移动。 functio
第一个问题,为什么不在 atomic_compare_exchange_weak 操作的参数中应用后缀求值 (++)?运算前后a的值相同。然而,当在 printf() 中使用时,正如预期的那样,该值会
我正在尝试使用 OpenMP 对已经矢量化的代码进行内部函数并行化,但问题是我使用一个 XMM 寄存器作为外部“变量”,我会在每个循环中递增。现在我正在使用 shared 子句 __m128d xmm
clojure“atom”的文档指出 - "Changes to atoms are always free of race conditions." 但是,竞争条件不仅根据更改定义,而且在不同线程中
我一直在研究原子引用计数的实现。 库之间的大多数操作都非常一致,但我在“减少引用计数”操作中发现了惊人的多样性。 (请注意,通常情况下,shared 和 weak decref 之间的唯一区别是调用了
我是一名优秀的程序员,十分优秀!