C ++中的“伪原子”操作

时间:2010-05-01 23:47:54

标签: c++ multithreading atomic volatile

所以我知道C ++中什么都不是原子的。但我想弄清楚我是否有任何“伪原子”假设。原因是我想避免在一些简单的情况下使用互斥体,我只需要非常弱的保证。

1)假设我有全局定义的volatile bool b,其中 最初我说的是真的。然后我启动一个执行循环的线程

while(b) doSomething();

同时,在另一个线程中,我执行b = true。

我可以假设第一个线程将继续执行吗?换句话说,如果b开始为true,并且第一个线程在第二个线程分配b = true的同时检查b的值,我可以假设第一个线程将b的值读为true吗?或者是否可能在赋值的某个中间点b = true,b的值可能被读为false?

2)现在假设b最初是假的。然后第一个线程执行

bool b1=b;
bool b2=b;
if(b1 && !b2) bad();

而第二个线程执行b = true。我可以假设bad()永远不会被调用吗?

3)int或其他内置类型怎么样:假设我有volatile int i,最初(比方说)7,然后我指定i = 7。我可以假设,在此操作期间的任何时间,从任何线程,i的值将等于7?

4)我有volatile int i = 7,然后我从某个线程执行i ++,所有其他线程只读取i的值。除了7或8之外,我可以假设我在任何线程中都没有任何价值吗?

5)我有volatile int i,从一个执行i = 7的线程,从另一个执行i = 8。之后,我保证是7或8(或者我选择分配的两个值)?

7 个答案:

答案 0 :(得分:14)

标准C ++中没有线程,Threads cannot be implemented as a library

因此,该标准对使用线程的程序的行为没有任何意义。您必须查看线程实现提供的任何其他保证。

那就是说,在我使用的线程实现中:

(1)是的,您可以假设不相关的值不会写入变量。否则整个内存模型就会消失。但要小心,当你说“另一个线程”从未将b设置为false时,这意味着任何地方。如果是这样,那么写入可能会在循环期间重新排序。

(2)不,编译器可以将分配重新排序为b1和b2,因此b1可能最终为真,而b2为假。在这么简单的情况下,我不知道它为什么要重新排序,但在更复杂的情况下可能有很好的理由。

[编辑:oops,当我回答时(2)我忘记了b是不稳定的。从一个volatile变量读取不会被重新排序,对不起,所以在典型的线程实现上是这样(如果有任何这样的事情),你可以假设你最终不会以b1为真,而b2为假。] < / p>

(3)与1. volatile相同通常与线程无关。但是,在某些实现(Windows)中它非常令人兴奋,并且可能实际上意味着内存障碍。

(4)在int写入的架构上是原子的,尽管volatile与它无关。另见......

(5)仔细检查文档。可能是的,并且volatile再次无关紧要,因为几乎所有架构int写入都是原子的。但是如果int写不是原子的,那么没有(前一个问题没有),即使它是不稳定的,你原则上可以获得不同的值。但是,鉴于这些值为7和8,我们正在讨论一个非常奇怪的架构,其中包含要在两个阶段写入的相关位的字节,但是使用不同的值可以更合理地获得部分写入。

对于一个更合理的例子,假设出于某种奇怪的原因,在平台上只有8位写入是原子的,那么你只有8位写入。奇怪但合法,并且由于int必须至少为16位,您可以看到它是如何产生的。进一步假设你的初始值是255.那么增量可以合法地实现为:

  • 读取旧值
  • 寄存器中的增量
  • 写出结果的最重要字节
  • 写出结果的最低有效字节。

一个只读线程,它在第三步和第四步之间中断递增线程,可以看到值511.如果写入是另一个顺序,则可以看到0.

如果一个线程写入255,另一个线程同时写入256,并且写入交错,则永久保留不一致的值。许多架构都不可能,但要知道这不会发生,你至少需要知道一些架构。 C ++标准中没有任何内容禁止它,因为C ++标准谈到执行被信号中断,但是否则没有执行的概念被程序的另一部分中断,也没有并发执行的概念。这就是为什么线程不仅仅是另一个库 - 添加线程从根本上改变了C ++执行模型。它要求实现以不同的方式执行操作,因为您最终会发现是否使用gcc下的线程并忘记指定-pthreads

同样的情况可能发生在对齐 int写入是原子的平台上,但是未对齐的int写入是允许的而不是原子的。例如,对于x86上的IIRC,如果未对齐的int写入超过高速缓存行边界,则它们不保证是原子的。由于这个原因,x86编译器不会错误地对齐声明的int变量。但如果你玩结构包装游戏,你可能会引发一个例子。

所以:几乎任何实现都会为您提供所需的保证,但可能会以相当复杂的方式完成。

总的来说,我发现不值得尝试依赖特定于平台的内存访问保证,我不完全理解,以避免互斥。使用互斥锁,如果速度太慢,请使用由真正了解架构和编译器的人编写的高质量无锁结构(或实现一个设计)。它可能是正确的,并且正确性可能会超出我自己创造的任何东西。

答案 1 :(得分:6)

大多数答案正确地解决了您将要体验的CPU内存排序问题,但没有一个能够详细说明编译器如何通过以破坏您的方式重新排序代码来阻止您的意图假设。

考虑一个取自this post的例子:

volatile int ready;       
int message[100];      

void foo(int i) 
{      
    message[i/10] = 42;      
    ready = 1;      
}

-O2及以上,最新版本的GCC和英特尔C / C ++(不了解VC ++)将首先存储到ready,因此它可以与{的计算重叠{1}}(i/10不会救你!):

volatile

这不是一个错误,它是利用CPU流水线的优化器。如果另一个帖子在访问 leaq _message(%rip), %rax movl $1, _ready(%rip) ; <-- whoa Nelly! movq %rsp, %rbp sarl $2, %edx subl %edi, %edx movslq %edx,%rdx movl $42, (%rax,%rdx,4) 的内容之前等待ready,那么你就会有一个令人讨厌且模糊不清的比赛。

使用编译器障碍以确保您的意图得到尊重。同样利用x86相对强大的排序的一个例子是Dmitriy Vyukov的单生产者单一消费者队列posted here中的发布/消费包装:

message

我建议如果您打算进入并发内存访问领域,请使用一个可以为您处理这些细节的库。我们都在等待n2145// load with 'consume' (data-dependent) memory ordering // NOTE: x86 specific, other platforms may need additional memory barriers template<typename T> T load_consume(T const* addr) { T v = *const_cast<T const volatile*>(addr); __asm__ __volatile__ ("" ::: "memory"); // compiler barrier return v; } // store with 'release' memory ordering // NOTE: x86 specific, other platforms may need additional memory barriers template<typename T> void store_release(T* addr, T v) { __asm__ __volatile__ ("" ::: "memory"); // compiler barrier *const_cast<T volatile*>(addr) = v; } 查看“主题构建模块”tbb::atomic或即将发布的boost::atomic

除了正确性之外,这些库可以简化您的代码并澄清您的意图:

std::atomic

使用显式内存排序,// thread 1 std::atomic<int> foo; // or tbb::atomic, boost::atomic, etc foo.store(1, std::memory_order_release); // thread 2 int tmp = foo.load(std::memory_order_acquire); 的线程间关系是明确的。

答案 2 :(得分:2)

可能这个线程很古老,但C ++ 11标准DOES有一个线程库,也有一个庞大的原子操作原子库。目的是专门用于并发支持并避免数据争用。 相关标头是原子

答案 3 :(得分:1)

依靠这一点通常是一个非常非常糟糕的主意,因为你最终可能会遇到一些不好的事情而且只有一些架构。最好的解决方案是使用有保证的原子API,例如Windows Interlocked api。

答案 4 :(得分:0)

如果您的C ++实现提供了由n2145或其某些变体指定的原子操作库,那么您可能会依赖它。否则,你通常不能在语言层面依赖关于原子性的“任何东西”,因为现有的C ++标准没有规定任何类型的多任务处理(因此处理多任务处理的原子性)。

答案 5 :(得分:0)

C ++中的易失性与Java中的作用不同。史蒂夫说,所有案件都是不明确的行为。对于编译器,给定的处理器体系结构和多线程系统,某些情况可能是好的,但切换优化标志可能会使您的程序行为不同,因为C ++ 03编译器不了解线程。

C ++ 0x定义了避免竞争条件的规则以及帮助您掌握该规则的操作,但是可能知道还没有编译器实现与该主题相关的标准的所有部分。

答案 6 :(得分:-1)

我的回答令人沮丧:不,不,不,不,不。

1-4)允许编译器使用它写入的变量来做任何事情。它可以在其中存储临时值,只要最终执行的操作与在真空中执行的线程执行相同的操作即可。任何有效的

5)不,不保证。如果变量不是原子的,并且您在一个线程上写入它,并在另一个线程上读取或写入它,那么它就是一个竞争案例。规范声明这种种族案例是未定义的行为,绝对是有的。话虽这么说,你很难找到一个不会给你7或8的编译器,但编译器给你别的东西是合法的。

我总是提到种族案例的这种高度滑稽的解释。

http://software.intel.com/en-us/blogs/2013/01/06/benign-data-races-what-could-possibly-go-wrong