C++ 我听说 i++ 不是线程安全的,++i 是线程安全的吗?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/680097/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
I've heard i++ isn't thread safe, is ++i thread-safe?
提问by samoz
I've heard that i++ isn't a thread-safe statement since in assembly it reduces down to storing the original value as a temp somewhere, incrementing it, and then replacing it, which could be interrupted by a context switch.
我听说 i++ 不是线程安全的语句,因为在汇编中它简化为将原始值作为临时值存储在某处,增加它,然后替换它,这可能会被上下文切换中断。
However, I'm wondering about ++i. As far as I can tell, this would reduce to a single assembly instruction, such as 'add r1, r1, 1' and since it's only one instruction, it'd be uninterruptable by a context switch.
但是,我想知道 ++i。据我所知,这将减少为一条汇编指令,例如“add r1, r1, 1”,并且由于它只是一条指令,因此不会被上下文切换中断。
Can anyone clarify? I'm assuming that an x86 platform is being used.
任何人都可以澄清吗?我假设正在使用 x86 平台。
回答by paxdiablo
You've heard wrong. It may well be that "i++"
is thread-safe for a specific compiler and specific processor architecture but it's not mandated in the standards at all. In fact, since multi-threading isn't part of the ISO C or C++ standards (a), you can't consider anything to be thread-safe based on what you think it will compile down to.
你听错了。"i++"
对于特定的编译器和特定的处理器架构来说,这很可能是线程安全的,但标准中根本没有强制要求。事实上,由于多线程不是 ISO C 或 C++ 标准(a) 的一部分,因此您不能根据您认为它将编译成什么来认为任何东西都是线程安全的。
It's quite feasible that ++i
could compile to an arbitrary sequence such as:
++i
可以编译为任意序列是非常可行的,例如:
load r0,[i] ; load memory into reg 0
incr r0 ; increment reg 0
stor [i],r0 ; store reg 0 back to memory
which would not be thread-safe on my (imaginary) CPU that has no memory-increment instructions. Or it may be smart and compile it into:
这在没有内存增量指令的我的(假想)CPU 上不是线程安全的。或者它可能很聪明并将其编译为:
lock ; disable task switching (interrupts)
load r0,[i] ; load memory into reg 0
incr r0 ; increment reg 0
stor [i],r0 ; store reg 0 back to memory
unlock ; enable task switching (interrupts)
where lock
disables and unlock
enables interrupts. But, even then, this may not be thread-safe in an architecture that has more than one of these CPUs sharing memory (the lock
may only disable interrupts for one CPU).
其中lock
禁用和unlock
启用中断。但是,即便如此,在具有多个 CPU 共享内存(lock
可能仅禁用一个 CPU 的中断)的体系结构中,这可能不是线程安全的。
The language itself (or libraries for it, if it's not built into the language) will provide thread-safe constructs and you should use those rather than depend on your understanding (or possibly misunderstanding) of what machine code will be generated.
语言本身(或它的库,如果它没有内置到语言中)将提供线程安全的构造,您应该使用这些构造,而不是依赖于您对将生成什么机器代码的理解(或可能的误解)。
Things like Java synchronized
and pthread_mutex_lock()
(available to C/C++ under some operating systems) are what you need to look into (a).
像 Javasynchronized
和pthread_mutex_lock()
(在某些操作系统下可用于 C/C++)之类的东西是您需要研究的(a)。
(a)This question was asked before the C11 and C++11 standards were completed. Those iterations have now introduced threading support into the language specifications, including atomic data types (though they, and threads in general, are optional,at least in C).
(a)这个问题是在 C11 和 C++11 标准完成之前提出的。这些迭代现在已经在语言规范中引入了线程支持,包括原子数据类型(尽管它们和一般的线程是可选的,至少在 C 中是这样)。
回答by Jim Mischel
You can't make a blanket statement about either ++i or i++. Why? Consider incrementing a 64-bit integer on a 32-bit system. Unless the underlying machine has a quad word "load, increment, store" instruction, incrementing that value is going to require multiple instructions, any of which can be interrupted by a thread context switch.
你不能对 ++i 或 i++ 做一个笼统的声明。为什么?考虑在 32 位系统上递增 64 位整数。除非底层机器有一个四字“加载、增加、存储”指令,否则增加该值将需要多条指令,其中任何一条都可以被线程上下文切换中断。
In addition, ++i
isn't always "add one to the value." In a language like C, incrementing a pointer actually adds the size of the thing pointed to. That is, if i
is a pointer to a 32-byte structure, ++i
adds 32 bytes. Whereas almost all platforms have an "increment value at memory address" instruction that is atomic, not all have an atomic "add arbitrary value to value at memory address" instruction.
此外,++i
并不总是“给值加一”。在像 C 这样的语言中,增加一个指针实际上增加了指向的东西的大小。也就是说,如果i
是指向 32 字节结构的指针,则++i
添加 32 字节。尽管几乎所有平台都有一个原子性的“在内存地址处增加值”指令,但并非所有平台都有一个原子性的“将任意值添加到内存地址处的值”指令。
回答by yogman
They are both thread-unsafe.
它们都是线程不安全的。
A CPU cannot do math directly with memory. It does that indirectly by loading the value from memory and doing the math with CPU registers.
CPU 不能直接用内存进行数学运算。它通过从内存加载值并使用 CPU 寄存器进行数学运算来间接实现这一点。
i++
我++
register int a1, a2;
a1 = *(&i) ; // One cpu instruction: LOAD from memory location identified by i;
a2 = a1;
a1 += 1;
*(&i) = a1;
return a2; // 4 cpu instructions
++i
++我
register int a1;
a1 = *(&i) ;
a1 += 1;
*(&i) = a1;
return a1; // 3 cpu instructions
For both cases, there is a race condition that results in the unpredictable i value.
对于这两种情况,都存在导致不可预测的 i 值的竞争条件。
For example, let's assume there are two concurrent ++i threads with each using register a1, b1 respectively. And, with context switching executed like the following:
例如,假设有两个并发 ++i 线程,每个线程分别使用寄存器 a1、b1。并且,上下文切换执行如下:
register int a1, b1;
a1 = *(&i);
a1 += 1;
b1 = *(&i);
b1 += 1;
*(&i) = a1;
*(&i) = b1;
In result, i doesn't become i+2, it becomes i+1, which is incorrect.
结果,我没有变成 i+2,而是变成了 i+1,这是不正确的。
To remedy this, moden CPUs provide some kind of LOCK, UNLOCK cpu instructions during the interval a context switching is disabled.
为了解决这个问题,现代 CPU 在上下文切换被禁用的时间间隔内提供了某种 LOCK、UNLOCK cpu 指令。
On Win32, use InterlockedIncrement() to do i++ for thread-safety. It's much faster than relying on mutex.
在 Win32 上,为了线程安全,使用 InterlockedIncrement() 来做 i++。它比依赖互斥锁快得多。
回答by Eclipse
If you are sharing even an int across threads in a multi-core environment, you need proper memory barriers in place. This can mean using interlocked instructions (see InterlockedIncrement in win32 for example), or using a language (or compiler) that makes certain thread-safe guarantees. With CPU level instruction-reordering and caches and other issues, unless you have those guarantees, don't assume anything shared across threads is safe.
如果您在多核环境中跨线程共享一个 int,则需要适当的内存屏障。这可能意味着使用互锁指令(例如,参见 win32 中的 InterlockedIncrement),或使用一种语言(或编译器)来保证某些线程安全。对于 CPU 级指令重新排序和缓存以及其他问题,除非您有这些保证,否则不要假设跨线程共享的任何内容都是安全的。
Edit: One thing you can assume with most architectures is that if you are dealing with properly aligned single words, you won't end up with a single word containing a combination of two values that were mashed together. If two writes happen over top of each other, one will win, and the other will be discarded. If you are careful, you can take advantage of this, and see that either ++i or i++ are thread-safe in the single writer/multiple reader situation.
编辑:对于大多数架构,您可以假设的一件事是,如果您正在处理正确对齐的单个单词,则最终不会得到一个包含两个混合在一起的值的组合的单词。如果两个写入发生在彼此之上,一个将获胜,另一个将被丢弃。如果您小心,您可以利用这一点,并看到 ++i 或 i++ 在单写入器/多读取器情况下是线程安全的。
回答by Max Lybbert
If you want an atomic increment in C++ you can use C++0x libraries (the std::atomic
datatype) or something like TBB.
如果您想要 C++ 中的原子增量,您可以使用 C++0x 库(std::atomic
数据类型)或类似 TBB 的东西。
There was once a time that the GNU coding guidelines said updating datatypes that fit in one word was "usually safe" but that advice is wrong for SMP machines, wrong for some architectures,and wrong when using an optimizing compiler.
曾几何时,GNU 编码指南说更新适合一个词的数据类型“通常是安全的”,但该建议对于 SMP 机器是错误的,对于某些体系结构是错误的,而在使用优化编译器时也是错误的。
To clarify the "updating one-word datatype" comment:
澄清“更新单字数据类型”注释:
It is possible for two CPUs on an SMP machine to write to the same memory location in the same cycle, and then try to propagate the change to the other CPUs and the cache. Even if only one word of data is being written so the writes only take one cycle to complete, they also happen simultaneously so you cannot guarantee which write succeeds. You won't get partially updated data, but one write will disappear because there is no other way to handle this case.
SMP 机器上的两个 CPU 可能在同一周期内写入同一内存位置,然后尝试将更改传播到其他 CPU 和缓存。即使只写入一个字的数据,因此写入只需一个周期即可完成,它们也会同时发生,因此您无法保证哪个写入成功。您不会获得部分更新的数据,但一次写入将消失,因为没有其他方法可以处理这种情况。
Compare-and-swap properly coordinates between multiple CPUs, but there is no reason to believe that every variable assignment of one-word datatypes will use compare-and-swap.
比较和交换在多个 CPU 之间正确协调,但没有理由相信单字数据类型的每个变量分配都会使用比较和交换。
And while an optimizing compiler doesn't affect howa load/store is compiled, it can change whenthe load/store happens, causing serious trouble if you expect your reads and writes to happen in the same order they appear in the source code (the most famous being double-checked locking does not work in vanilla C++).
虽然一个优化编译器不会影响如何加载/存储编译,它可以改变时,加载/存储发生,如果你希望你的读取和写入在它们出现在源代码相同的顺序发生(,造成了严重的麻烦最著名的是双重检查锁定在普通 C++ 中不起作用)。
NOTEMy original answer also said that Intel 64 bit architecture was broken in dealing with 64 bit data. That is not true, so I edited the answer, but my edit claimed PowerPC chips were broken. That is true when reading immediate values (i.e., constants) into registers(see the two sections named "Loading pointers" under listing 2 and listing 4) . But there is an instruction for loading data from memory in one cycle (lmw
), so I've removed that part of my answer.
注意我原来的回答还说英特尔 64 位架构在处理 64 位数据时被破坏了。这不是真的,所以我编辑了答案,但我的编辑声称 PowerPC 芯片坏了。 当将立即数(即常量)读入寄存器时,情况确实如此(参见清单 2 和清单 4 下名为“加载指针”的两节)。但是有一条指令可以在一个周期内从内存中加载数据 ( lmw
),所以我已经删除了答案的那部分。
回答by i_am_jorf
On x86/Windows in C/C++, you should not assume it is thread-safe. You should use InterlockedIncrement()and InterlockedDecrement()if you require atomic operations.
在 C/C++ 中的 x86/Windows 上,您不应假定它是线程安全的。如果您需要原子操作,您应该使用InterlockedIncrement()和InterlockedDecrement()。
回答by xtofl
If your programming language says nothing about threads, yet runs on a multithreaded platform, how can anylanguage construct be thread-safe?
如果您的编程语言对线程一无所知,但在多线程平台上运行,那么任何语言构造怎么能是线程安全的?
As others pointed out: you need to protect any multithreaded access to variables by platform specific calls.
正如其他人指出的那样:您需要通过特定于平台的调用来保护对变量的任何多线程访问。
There are libraries out there that abstract away the platform specificity, and the upcoming C++ standard has adapted it's memory model to cope with threads (and thus can guarantee thread-safety).
有一些库可以抽象出平台的特殊性,而即将到来的 C++ 标准已经调整了它的内存模型来处理线程(因此可以保证线程安全)。
回答by CesarB
Even if it is reduced to a single assembly instruction, incrementing the value directly in memory, it is still not thread safe.
即使将其缩减为单个汇编指令,直接在内存中递增值,它仍然不是线程安全的。
When incrementing a value in memory, the hardware does a "read-modify-write" operation: it reads the value from the memory, increments it, and writes it back to memory. The x86 hardware has no way of incrementing directly on the memory; the RAM (and the caches) is only able to read and store values, not modify them.
当增加内存中的值时,硬件会执行“读-修改-写”操作:它从内存中读取值,将其递增,然后将其写回内存。x86 硬件无法直接在内存上递增;RAM(和缓存)只能读取和存储值,不能修改它们。
Now suppose you have two separate cores, either on separate sockets or sharing a single socket (with or without a shared cache). The first processor reads the value, and before it can write back the updated value, the second processor reads it. After both processors write the value back, it will have been incremented only once, not twice.
现在假设您有两个独立的内核,要么位于不同的套接字上,要么共享一个套接字(有或没有共享缓存)。第一个处理器读取该值,在它可以写回更新后的值之前,第二个处理器读取它。在两个处理器都将值写回后,它只会增加一次,而不是两次。
There is a way to avoid this problem; x86 processors (and most multi-core processors you will find) are able to detect this kind of conflict in hardware and sequence it, so that the whole read-modify-write sequence appears atomic. However, since this is very costly, it is only done when requested by the code, on x86 usually via the LOCK
prefix. Other architectures can do this in other ways, with similar results; for instance, load-linked/store-conditional and atomic compare-and-swap (recent x86 processors also have this last one).
有一种方法可以避免这个问题;x86 处理器(以及您会发现的大多数多核处理器)能够检测硬件中的这种冲突并对其进行排序,从而使整个读取-修改-写入序列看起来是原子的。但是,由于这非常昂贵,因此仅在代码请求时才完成,在 x86 上通常通过LOCK
前缀。其他架构可以通过其他方式做到这一点,结果相似;例如,加载链接/存储条件和原子比较和交换(最近的 x86 处理器也有最后一个)。
Note that using volatile
does not help here; it only tells the compiler that the variable might have be modified externally and reads to that variable must not be cached in a register or optimized out. It does not make the compiler use atomic primitives.
请注意,使用volatile
在这里没有帮助;它只告诉编译器该变量可能已被外部修改,并且对该变量的读取不得缓存在寄存器中或优化掉。它不会让编译器使用原子原语。
The best way is to use atomic primitives (if your compiler or libraries have them), or do the increment directly in assembly (using the correct atomic instructions).
最好的方法是使用原子原语(如果你的编译器或库有它们),或者直接在汇编中进行增量(使用正确的原子指令)。
回答by Selso Liberado
According to this assembly lessonon x86, you can atomically add a register to a memory location, so potentially your code may atomically execute '++i' ou 'i++'. But as said in another post, the C ansi does not apply atomicity to '++' opération, so you cannot be sure of what your compiler will generate.
根据x86上的这个汇编课程,您可以原子地将寄存器添加到内存位置,因此您的代码可能会原子地执行 '++i' 或 'i++'。但是正如在另一篇文章中所说,C ansi 不会将原子性应用于“++”操作,因此您无法确定编译器会生成什么。
回答by Dan Olson
Never assume that an increment will compile down to an atomic operation. Use InterlockedIncrement or whatever similar functions exist on your target platform.
永远不要假设增量会编译为原子操作。使用 InterlockedIncrement 或目标平台上存在的任何类似函数。
Edit: I just looked up this specific question and increment on X86 is atomic on single processor systems, but not on multiprocessor systems. Using the lock prefix can make it atomic, but it's much more portable just to use InterlockedIncrement.
编辑:我刚刚查了这个特定的问题,X86 上的增量在单处理器系统上是原子的,但在多处理器系统上不是。使用 lock 前缀可以使其原子化,但仅使用 InterlockedIncrement 就更加便携。