简体   繁体   English

我听说 i++ 不是线程安全的,++i 是线程安全的吗?

[英]I've heard i++ isn't thread safe, is ++i thread-safe?

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.但是,我想知道 ++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.据我所知,这将减少为单个汇编指令,例如“add r1, r1, 1”,并且由于它只是一个指令,因此它不会被上下文切换中断。

Can anyone clarify?谁能澄清一下? I'm assuming that an x86 platform is being used.我假设正在使用 x86 平台。

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.很可能"i++"对于特定的编译器和特定的处理器架构是线程安全的,但它根本不是标准中强制要求的。 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.事实上,由于多线程不是 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.这在我没有内存递增指令的(假想的)CPU 上不是线程安全的。 Or it may be smart and compile it into:或者它可能很聪明,将它编译成:

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.其中lock禁用和unlock启用中断。 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).但是,即便如此,在具有多个 CPU 共享 memory 的架构中,这可能不是线程安全的( 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) .诸如 Java synchronizedpthread_mutex_lock() (某些操作系统下的 C/C++ 可用)之类的东西是您需要研究的 (a)


(a) This question was asked before the C11 and C++11 standards were completed. (a)这个问题是在C11和C++11标准完成之前问的。 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).这些迭代现在已经在语言规范中引入了线程支持,包括原子数据类型(尽管它们和一般的线程是可选的,至少在 C 语言中是这样)。

You can't make a blanket statement about either ++i or i++.您不能对 ++i 或 i++ 做出笼统的陈述。 Why?为什么? Consider incrementing a 64-bit integer on a 32-bit system.考虑在 32 位系统上递增 64 位 integer。 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.除非底层机器有一个四字“加载、递增、存储”指令,否则递增该值将需要多条指令,其中任何一条指令都可以被线程上下文切换中断。

In addition, ++i isn't always "add one to the value."此外, ++i并不总是“将值加一”。 In a language like C, incrementing a pointer actually adds the size of the thing pointed to.在像 C 这样的语言中,递增指针实际上会增加所指向事物的大小。 That is, if i is a pointer to a 32-byte structure, ++i adds 32 bytes.也就是说,如果i是一个指向 32 字节结构的指针,则++i添加 32 个字节。 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.尽管几乎所有平台都有原子性的“memory 地址处的增量值”指令,但并非所有平台都有原子性的“将任意值添加到 memory 地址处的值”指令。

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 不能直接对 memory 进行数学运算。它通过从 memory 加载值并使用 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.例如,假设有两个并发的 ++i 线程,每个线程分别使用寄存器 a1、b1。 And, with context switching executed like the following:并且,上下文切换执行如下:

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 没有变成 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.在 Win32 上,使用 InterlockedIncrement() 执行 i++ 以确保线程安全。 It's much faster than relying on mutex.它比依赖互斥锁快得多。

If you are sharing even an int across threads in a multi-core environment, you need proper memory barriers in place.如果您在多核环境中跨线程共享一个 int,则需要适当的 memory 屏障。 This can mean using interlocked instructions (see InterlockedIncrement in win32 for example), or using a language (or compiler) that makes certain thread-safe guarantees.这可能意味着使用互锁指令(例如,请参阅 win32 中的 InterlockedIncrement),或使用可提供某些线程安全保证的语言(或编译器)。 With CPU level instruction-reordering and caches and other issues, unless you have those guarantees, don't assume anything shared across threads is safe.对于 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++ 在单写入器/多读取器情况下是线程安全的。

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. SMP 机器上的两个 CPU 可能会在同一周期内写入相同的 memory 位置,然后尝试将更改传播到其他 CPU 和缓存。 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.您不会获得部分更新的数据,但一次写入将消失,因为没有其他方法可以处理这种情况。

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 how a load/store is compiled, it can change when the 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++ 中不起作用)。

NOTE My original answer also said that Intel 64 bit architecture was broken in dealing with 64 bit data.注意我原来的回答还说英特尔 64 位架构在处理 64 位数据时被破坏了。 That is not true, so I edited the answer, but my edit claimed PowerPC chips were broken.那不是真的,所以我编辑了答案,但我的编辑声称 PowerPC 芯片坏了。 That is true when reading immediate values (ie, constants) into registers (see the two sections named "Loading pointers" under listing 2 and listing 4).将立即值(即常量)读入寄存器时也是如此(参见清单 2 和清单 4 下名为“加载指针”的两节)。 But there is an instruction for loading data from memory in one cycle ( lmw ), so I've removed that part of my answer.但是有一条在一个周期 ( lmw ) 中从 memory 加载数据的说明,所以我删除了那部分答案。

Even if it is reduced to a single assembly instruction, incrementing the value directly in memory, it is still not thread safe.即使缩减为单条汇编指令,直接在memory中自增,依然不是线程安全的。

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;当memory的值递增时,硬件会做一个“读-修改-写”的操作:从memory读取值,递增,写回memory。x86硬件没有办法直接在memory递增; the RAM (and the caches) is only able to read and store values, not modify them. RAM(和缓存)只能读取和存储值,不能修改它们。

Now suppose you have two separate cores, either on separate sockets or sharing a single socket (with or without a shared cache).现在假设您有两个独立的核心,或者在单独的 sockets 上,或者共享一个套接字(有或没有共享缓存)。 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. x86 处理器(以及您会发现的大多数多核处理器)能够在硬件中检测到这种冲突并对其进行排序,因此整个读取-修改-写入序列看起来是原子的。 However, since this is very costly, it is only done when requested by the code, on x86 usually via the LOCK prefix.然而,由于这是非常昂贵的,所以只有在代码请求时才会这样做,在 x86 上通常通过LOCK前缀。 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 处理器也有最后一个)。

Note that using volatile does not help here;请注意,使用volatile在这里没有帮助; 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.它不会使编译器使用原子原语。

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).最好的方法是使用原子原语(如果您的编译器或库有它们),或者直接在汇编中执行增量(使用正确的原子指令)。

On x86/Windows in C/C++, you should not assume it is thread-safe.在使用 C/C++ 的 x86/Windows 上,您不应假定它是线程安全的。 You should use InterlockedIncrement() and InterlockedDecrement() if you require atomic operations.如果需要原子操作,则应使用InterlockedIncrement()InterlockedDecrement()

If your programming language says nothing about threads, yet runs on a multithreaded platform, how can any language 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++ 标准已经调整了它的 memory model 来处理线程(因此可以保证线程安全)。

According to this assembly lesson on x86, you can atomically add a register to a memory location , so potentially your code may atomically execute '++i' ou 'i++'.根据 x86 上的汇编课程,您可以自动将寄存器添加到 memory 位置,因此您的代码可能会自动执行 '++i' 或 '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.但正如在另一篇文章中所说,C ansi 不会将原子性应用于“++”操作,因此您无法确定您的编译器将生成什么。

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.编辑:我刚刚查看了这个特定问题,X86 的增量在单处理器系统上是原子的,但在多处理器系统上不是。 Using the lock prefix can make it atomic, but it's much more portable just to use InterlockedIncrement.使用 lock 前缀可以使其成为原子的,但仅使用 InterlockedIncrement 就可移植得多。

Throw i into thread local storage;将 i 放入线程本地存储; it isn't atomic, but it then doesn't matter.它不是原子的,但是没关系。

AFAIK, According to the C++ standard, read/writes to an int are atomic. AFAIK,根据 C++ 标准,对int的读/写是原子的。

However, all that this does is get rid of the undefined behavior that's associated with a data race.但是,这所做的只是摆脱与数据竞争相关的未定义行为。

But there still will be a data race if both threads try to increment i .但是如果两个线程都试图增加i ,仍然会出现数据竞争。

Imagine the following scenario:想象一下以下场景:

Let i = 0 initially:i = 0初始:

Thread A reads the value from memory and stores in its own cache.线程 A 从 memory 中读取值并存储在自己的缓存中。 Thread A increments the value by 1.线程 A 将值递增 1。

Thread B reads the value from memory and stores in its own cache.线程 B 从 memory 中读取值并存储在自己的缓存中。 Thread B increments the value by 1.线程 B 将值递增 1。

If this is all a single thread you would get i = 2 in memory.如果这都是单线程,您将在 memory 中得到i = 2

But with both threads, each thread writes its changes and so Thread A writes i = 1 back to memory, and Thread B writes i = 1 to memory.但是对于两个线程,每个线程都会写入它的更改,因此线程 A 将i = 1写回 memory,而线程 B 将i = 1写回 memory。

It's well defined, there's no partial destruction or construction or any sort of tearing of an object, but it's still a data race.它定义明确,没有部分破坏或构造或任何类型的 object 撕裂,但它仍然是一场数据竞赛。

In order to atomically increment i you can use:为了自动递增i你可以使用:

std::atomic<int>::fetch_add(1, std::memory_order_relaxed)

Relaxed ordering can be used because we don't care where this operation takes place all we care about is that the increment operation is atomic.可以使用宽松的顺序,因为我们不关心这个操作发生在哪里,我们只关心增量操作是原子的。

The 1998 C++ standard has nothing to say about threads, although the next standard (due this year or the next) does. 1998 年的 C++ 标准没有提到线程,尽管下一个标准(今年或明年到期)有。 Therefore, you can't say anything intelligent about thread-safety of operations without referring to the implementation.因此,如果不参考实现,就不能说任何关于操作的线程安全性的智能信息。 It's not just the processor being used, but the combination of the compiler, the OS, and the thread model.不仅仅是正在使用的处理器,而是编译器、操作系统和线程 model 的组合。

In the absence of documentation to the contrary, I wouldn't assume that any action is thread-safe, particularly with multi-core processors (or multi-processor systems).在没有相反文档的情况下,我不会假设任何操作都是线程安全的,尤其是对于多核处理器(或多处理器系统)。 Nor would I trust tests, as thread synchronization problems are likely to come up only by accident.我也不相信测试,因为线程同步问题很可能只是偶然出现的。

Nothing is thread-safe unless you have documentation that says it is for the particular system you're using.除非您有说明它适用于您正在使用的特定系统的文档,否则没有什么是线程安全的。

You say "it's only one instruction, it'd be uninterruptible by a context switch."你说“这只是一条指令,它不会被上下文切换中断。” - that's all well and good for a single CPU, but what about a dual core CPU? - 这对单 CPU 来说很好,但是双核 CPU 呢? Then you can really have two threads accessing the same variable at the same time without any context switches.然后你真的可以让两个线程同时访问同一个变量而无需任何上下文切换。

Without knowing the language, the answer is to test the heck out of it.在不知道该语言的情况下,答案是对其进行测试。

I think that if the expression "i++" is the only in a statement, it's equivalent to "++i", the compiler is smart enough to not keep a temporal value, etc. So if you can use them interchangeably (otherwise you won't be asking which one to use), it doesn't matter whichever you use as they're almost the same (except for aesthetics).我认为如果表达式“i++”在一条语句中是唯一的,它等同于“++i”,编译器足够聪明,不会保留时间值等。所以如果你可以互换使用它们(否则你赢了不要问使用哪个),无论您使用哪个都没有关系,因为它们几乎相同(美学除外)。

Anyway, even if the increment operator is atomic, that doesn't guarantee that the rest of the computation will be consistent if you don't use the correct locks.无论如何,即使增量运算符是原子的,如果您不使用正确的锁,也不能保证计算的 rest 是一致的。

If you want to experiment by yourself, write a program where N threads increment concurrently a shared variable M times each... if the value is less than N*M, then some increment was overwritten.如果您想自己试验,请编写一个程序,其中 N 个线程同时递增一个共享变量 M 次,每个...如果该值小于 N*M,则某些递增被覆盖。 Try it with both preincrement and postincrement and tell us;-)试试预增量和后增量,然后告诉我们;-)

For a counter, I recommend a using the compare and swap idiom which is both non locking and thread-safe.对于计数器,我建议使用非锁定和线程安全的比较和交换习惯用法。

Here it is in Java:这是在 Java 中:

public class IntCompareAndSwap {
    private int value = 0;

    public synchronized int get(){return value;}

    public synchronized int compareAndSwap(int p_expectedValue, int p_newValue){
        int oldValue = value;

        if (oldValue == p_expectedValue)
            value = p_newValue;

        return oldValue;
    }
}

public class IntCASCounter {

    public IntCASCounter(){
        m_value = new IntCompareAndSwap();
    }

    private IntCompareAndSwap m_value;

    public int getValue(){return m_value.get();}

    public void increment(){
        int temp;
        do {
            temp = m_value.get();
        } while (temp != m_value.compareAndSwap(temp, temp + 1));

    }

    public void decrement(){
        int temp;
        do {
            temp = m_value.get();
        } while (temp > 0 && temp != m_value.compareAndSwap(temp, temp - 1));

    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM