繁体   English   中英

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

[英]“pseudo-atomic” operations in C++

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

1)假设我有全局定义的volatile bool b,最初我设置为true。 然后我启动一个执行循环的线程

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(或者我选择分配的两个值)?

标准C ++中没有线程,并且Threads不能实现为库

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

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

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

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

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

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

(4)在一个体系结构中, int写入是原子的,尽管volatile与它无关。 也可以看看...

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

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

  • 读旧值
  • 在寄存器中递增
  • 写下结果的最重要字节
  • 写下结果的最低有效字节。

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

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

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

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

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

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

考虑一下这篇文章的一个例子:

volatile int ready;       
int message[100];      

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

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

    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)

这不是一个错误,它是利用CPU流水线的优化器。 如果在访问message内容之前另一个线程正在等待ready ,那么你就会有一个令人讨厌且模糊不清的竞赛。

采用编译器障碍以确保您的意图得到尊重。 还利用86的相对强排序的例子是释放/消耗德米特里Vyukov的单生产者单消费者队列中找到包装贴在这里

// 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; 
} 

我建议如果您打算进入并发内存访问领域,请使用一个可以为您处理这些细节的库。 虽然我们都在等待n2145std::atomic检查Thread Building Blocks的tbb::atomic或即将推出的boost::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);

使用显式内存排序, foo的线程间关系很明确。

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

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

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

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

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

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

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

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

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

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

暂无
暂无

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

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