C ++中的原子性:神话还是现实

时间:2011-02-15 09:48:56

标签: c++ atomic memory-alignment

我一直在阅读有关MSDN中Lockless Programming的文章。它说:

  

在所有现代处理器上,您都可以   假设读写   自然对齐的原生类型是原子。只要内存总线是   至少与类型一样宽   读取或写入,CPU读取和   将这些类型写入单个总线   交易,使其无法实现   其他线程在一个看到它们   半完成状态。

它给出了一些例子:

// This write is not atomic because it is not natively aligned.
DWORD* pData = (DWORD*)(pChar + 1);
*pData = 0;

// This is not atomic because it is three separate operations.
++g_globalCounter;

// This write is atomic.
g_alignedGlobal = 0;

// This read is atomic.
DWORD local = g_alignedGlobal;

我读了很多答案和评论说,在C ++中没有任何保证是原子的,甚至在标准中都没有提到,在SO中,现在我有点困惑。我误解了这篇文章吗?或者文章作者是否谈论了非标准且特定于MSVC ++编译器的内容?

所以根据文章,下面的作业必须是原子的,对吗?

struct Data
{
    char ID;
    char pad1[3];
    short Number;
    char pad2[2];
    char Name[5];
    char pad3[3];
    int Number2;
    double Value;
} DataVal;

DataVal.ID = 0;
DataVal.Number = 1000;
DataVal.Number2 = 0xFFFFFF;
DataVal.Value = 1.2;

如果确实如此,用Name[5]替换pad3[3]std::string Name;会不会对内存对齐产生任何影响? Number2Value变量的分配是否仍然是原子的?

有人可以解释一下吗?

8 个答案:

答案 0 :(得分:29)

此建议特定于体系结构。 x86& x86_64(在低级编程中)。您还应检查编译器是否重新排序代码。你可以使用“编译器内存屏障”。

英特尔参考手册“英特尔®64和IA-32架构软件开发人员手册”第3A卷(http://www.intel.com/Assets/PDF/manual/253668.pdf),第8.1.1节

中介绍了x86的低级原子读写操作。
  

8.1.1保证原子操作

     

Intel486处理器(以及更新的处理器)保证以下内容   基本的内存操作将始终以原子方式执行:

  • 读取或写入字节
  • 读取或写入在16位边界上对齐的字
  • 读取或写入在32位边界上对齐的双字
  

Pentium处理器(以及更新的处理器)保证以下内容   额外的内存操作将始终以原子方式执行:

  • 读取或写入在64位边界上对齐的四字
  • 16位访问适合32位数据总线的未缓存内存位置
  

P6系列处理器(以及更新的处理器)保证以下内容   额外的内存操作将始终以原子方式执行:

  • 对缓存中适合的高速缓存内存的16位,32位和64位未对齐访问 线

对于像Core2这样的新处理器,本文档还有更多原子级的描述。 并非所有未对齐的操作都是原子操作。

其他英特尔手册推荐此白皮书:

http://software.intel.com/en-us/articles/developing-multithreaded-applications-a-platform-consistent-approach/

答案 1 :(得分:12)

我认为你误解了这句话。

Atomicity 可以使用特定指令(适用于此架构)在给定体系结构上得到保证。 MSDN文章解释说,在x86架构上,C ++内置类型的读写可以是 atomic

然而,C ++标准并没有假定架构是什么,因此标准不能做出这样的保证。实际上,C ++用于嵌入式软件,其中硬件支持更加有限。

C ++ 0x定义了std::atomic模板类,它允许将读取和写入转换为 atomic 操作,无论何种类型。编译器将根据类型特征和以标准兼容方式定位的体系结构选择获得原子性的最佳方法。

新标准还定义了许多类似于MSVC InterlockExchange的操作,这些操作也被编译为硬件提供的最快(但安全)可用的原语。

答案 2 :(得分:3)

c ++标准不保证原子行为。实际上,正如文章所述,简单的加载和存储操作将是原子操作。

如果你需要原子性,最好明确它并使用某种锁定。

*counter = 0; // this is atomic on most platforms
*counter++;   // this is NOT atomic on most platforms

答案 3 :(得分:2)

在依赖简单字大小操作的原子性时要非常小心,因为事情的行为可能与您期望的不同。在多核架构上,您可能会看到无序的读写操作。这将需要内存屏障来防止。 (更多细节here)。

应用程序开发人员的底线是使用操作系统保证原子的原语或使用适当的锁。

答案 4 :(得分:1)

IMO,文章结合了对底层架构的一些假设。由于C ++对架构只有一些简约的要求,因此不能在标准中给出关于原子性的保证。例如,一个字节必须至少为8位,但是你可以有一个字节为9位的架构,但理论上是一个int ... ...

因此,当编译器特定于x86架构时,可以使用特定功能。

注意:结构通常默认情况下与原生单词边界对齐。您可以通过#pragma语句禁用它,因此不需要填充填充

答案 5 :(得分:1)

我认为他们试图解决的问题是,硬件本身实现的数据类型是在硬件内更新的,这样从另一个线程读取将永远不会给你“部分”更新的值。

考虑32位机器上的32位整数。它在1个指令周期内写入或读取完全,而较大尺寸的数据类型,比如32位机器上的64位int将需要更多周期,因此理论上写入它们的线程可能会被中断在这些周期之间,这个值没有处于有效状态。

没有使用字符串不会使其成为原子,因为字符串是更高级别的构造,而不是在硬件中实现。 编辑:根据您对(不)更改为字符串的含义的评论,它不应对后面声明的字段产生任何影响,如另一个答案所述,编译器默认情况下会对齐字段。

它不在标准中的原因是,正如文章中所述,这是关于现代处理器如何实现指令。您的标准C / C ++代码应该在16或64位机器上完全相同(只是性能差异),但是如果您假设您只在64位机器上执行,那么64位或更小的任何内容都是原子的。 (SSE等类型除外)

答案 6 :(得分:1)

我认为文章中提到的atomicity几乎没有实际用途。这意味着您将读取/写入有效值但可能已过时。因此,读取一个int,你将完全读取它,而不是来自旧值的2个字节和来自另一个线程当前正在写入的新值的其他2个字节。

共享内存的重要性在于内存障碍。并且它们受到同步原语的保证,例如C ++ 0x atomic类型,mutexes等。

答案 7 :(得分:0)

我不认为将char Name[5]更改为std::string Name会产生差异如果您仅将其用于单个字符分配,因为索引运算符将返回直接引用对于潜在的角色。一个完整的字符串赋值不是原子的(你不能用char数组来做,所以我猜你也没想过以这种方式使用它。)