34

我一直在阅读MSDN中有关无锁编程的文章。它说 :

在所有现代处理器上,您可以假设 自然对齐的本机类型的读取和写入是原子的。只要内存总线至少与正在读取或写入的类型一样宽,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;内存对齐有什么影响吗?Number2对变量和变量的赋值Value仍然是原子的吗?

有人可以解释一下吗?

4

8 回答 8

30

此建议是特定于体系结构的。对于 x86 和 x86_64(在低级编程中)也是如此。您还应该检查编译器不会重新排序您的代码。您可以为此使用“编译器内存屏障”。

英特尔参考手册“英特尔® 64 和 IA-32 架构软件开发人员手册”第 3A 卷 ( http://www.intel.com/Assets/PDF/manual/253668 ) 中描述了 x86 的低级原子读写。 pdf),第 8.1.1 节

8.1.1 保证原子操作

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

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

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

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

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

  • 对适合高速缓存行的高速缓存内存进行未对齐的 16、32 和 64 位访问

该文档还对 Core2 等较新的处理器进行了原子化的更多描述。并非所有未对齐的操作都是原子的。

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

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

于 2011-02-15T10:09:50.410 回答
12

我认为您误解了报价。

可以使用特定指令(适用于该架构)在给定架构上保证原子性。MSDN 文章解释说,对 C++ 内置类型的读取和写入可以预期在架构上是原子的。x86

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

C++0x 定义了std::atomic模板类,它允许将读取和写入转换为原子操作,无论是什么类型。编译器将根据类型特征和以符合标准的方式所针对的体系结构选择最佳方式来获得原子性。

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

于 2011-02-15T10:47:22.427 回答
3

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

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

*counter = 0; // this is atomic on most platforms
*counter++;   // this is NOT atomic on most platforms
于 2011-02-15T09:55:22.333 回答
2

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

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

于 2011-02-15T10:00:26.950 回答
1

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

对于共享内存来说重要的是内存屏障。并且它们由诸如 C++0xatomic类型mutexes等同步原语保证。

于 2011-02-15T11:48:48.577 回答
1

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

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

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

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

于 2011-02-15T10:00:34.820 回答
1

IMO,这篇文章包含了一些关于底层架构的假设。由于 C++ 对体系结构只有一些简单的要求,因此标准中不能保证例如原子性。例如,一个字节必须至少为 8 位,但您可以拥有一个字节为 9 位但 int 16 的架构......理论上。

所以当编译器专门针对 x86 架构时,可以使用特定的功能。

注意:结构通常默认对齐到本地单词边界。您可以通过#pragma 语句禁用它,因此不需要填充填充

于 2011-02-15T09:57:10.930 回答
0

如果您仅将其用于单个字符分配,我认为更改char Name[5]std::string Name不会有什么不同,因为索引运算符将返回对基础字符的直接引用。一个完整的字符串赋值不是原子的(你不能用一个 char 数组来做,所以我猜你并没有考虑以这种方式使用它)。

于 2011-02-15T10:02:59.267 回答