- 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/
我正在尝试使用 y 组合器在 Scala 中定义 gcd: object Main { def y[A,B]( f : (A => B) => A => B ) : A => B = f(y(f)
我正在尝试了解返回指向函数的指针的函数,在我尝试编译代码后,它给了我这种错误: cannot convert int (*(int))(int) to int (*(int))(int) in ass
所以我一直在关注 youtube 上的游戏编程教程,然后弹出了这段代码:bufferedImageObject.getRGB(int, int, int, int, int[], int, int);
我正在将时间现在 与存储在数据库某处的时间进行比较。数据库中存储的时间格式为“yyyyMMddHHmmss”。例如,数据库可能会为存储的时间值返回 201106203354。然后我使用一个函数将时间现
例如 Maze0.bmp (0,0) (319,239) 65 120 Maze0.bmp (0,0) (319,239) 65 120 (254,243,90) Maze0.bmp (0,0) (
评论 Steve Yegge的post关于 server-side Javascript开始讨论语言中类型系统的优点和这个 comment描述: ... examples from H-M style
我正在研究 C 的指针,从 Deitel 的书中我不明白 int(*function)(int,int) 和 int*function(int, int) 表示函数时。 最佳答案 C 中读取类型的经验
您好,我使用 weblogic 11g 创建 war 应用程序,我对 joda time 的方法有疑问 new DateTime(int, int, int, int, int, int); 这抛出了
Create a method called average that calculates the average of the numbers passed as parameters. The
var a11: Int = 0 var a12: Int = 0 var a21: Int = 0 var a22: Int = 0 var valueDeterminant = a11 * a12
我正在为一个项目设置 LED 阵列。我得到了一个 LED 阵列,可以根据引脚变化电压进行更改,但我无法添加更多引脚。 当我尝试时,编译失败并显示错误:函数“int getMode(int, int,
除了创建对列表执行简单操作的函数之外,我对 haskell 还是很陌生。我想创建一个列表,其中包含 Int 类型的内容, 和 Int -> Int -> Int 类型的函数. 这是我尝试过的: dat
这个问题已经有答案了: Java add buttons dynamically as an array [duplicate] (4 个回答) 已关闭 7 年前。 StackOverFlow问题今天
我有几个 EditText View ,我想在其中设置左侧的图像,而 setCompoundDrawablesWithIntrinsicBounds 似乎不起作用。图形似乎没有改变。 有人知道为什么会
#include using namespace std; int main() { static_assert(is_constructible, int(*)(int,int)>::val
fun sum(a: Int, b: Int) = a + b val x = 1.to(2) 我在找: sum.tupled(x),或者 sum(*x) 当然,以上都不能用 Kotlin 1.1.3
有一个函数: func (first: Int) -> Int -> Bool -> String { return ? } 返回值怎么写?我对上面 func 的返回类型感到很困惑。 最
type foo = A of int * int | B of (int * int) int * int 和 (int * int) 有什么区别?我看到的唯一区别在于模式匹配: let test_
我正在尝试制作一个 slider 游戏。在这个类中,我使用 Graphics 对象 g2 的 drawImage 方法来显示“拼图”的 block 。但在绘制类方法中,我收到此错误:找不到符号方法dr
我试着理解这个表达: static Func isOdd = i => (i & 1) == 1; 但是这是什么意思呢? 例如我有 i = 3。然后 (3 & 1) == 1 或 i = 4。然后
我是一名优秀的程序员,十分优秀!