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

发布于 2024-10-17 20:02:47 字数 1238 浏览 1 评论 0原文

我一直在 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;

如果这是真的,用 std::string Name; 替换 Name[5]pad3[3] 是否会对内存产生任何影响 -结盟 ?对 Number2Value 变量的赋值仍然是原子的吗?

有人可以解释一下吗?

I have been reading an article about Lockless Programming in MSDN. It says :

On all modern processors, you can
assume that reads and writes of
naturally aligned native types are atomic. As long as the memory bus is
at least as wide as the type being
read or written, the CPU reads and
writes these types in a single bus
transaction, making it impossible for
other threads to see them in a
half-completed state.

And it gives some examples:

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

I read lots of answers and comments saying, nothing is guaranteed to be atomic in C++ and it is not even mentioned in standarts, in SO and now I am a bit confused. Am I misinterpreting the article? Or does the article writer talk about things that are non-standart and specific to MSVC++ compiler?

So according to the article the below assignments must be atomic, right?

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;

If it is true, does replacing Name[5] and pad3[3] with std::string Name; make any difference in memory-alignment ? Will the assignments to Number2 and Value variables be still atomic?

Can someone please explain?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

鹿港小镇 2024-10-24 20:02:47

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

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

8.1.1 有保证的原子操作

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

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

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

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

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

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

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

其他intel手册推荐此白皮书:

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

This recommendation is architecture-specific. It is true for x86 & x86_64 (in a low-level programming). You should also check that compiler don't reorder your code. You can use "compiler memory barrier" for that.

Low-level atomic read and writes for x86 is described in Intel Reference manuals "The Intel® 64 and IA-32 Architectures Software Developer’s Manual" Volume 3A ( http://www.intel.com/Assets/PDF/manual/253668.pdf) , section 8.1.1

8.1.1 Guaranteed Atomic Operations

The Intel486 processor (and newer processors since) guarantees that the following
basic memory operations will always be carried out atomically:

  • Reading or writing a byte
  • Reading or writing a word aligned on a 16-bit boundary
  • Reading or writing a doubleword aligned on a 32-bit boundary

The Pentium processor (and newer processors since) guarantees that the following
additional memory operations will always be carried out atomically:

  • Reading or writing a quadword aligned on a 64-bit boundary
  • 16-bit accesses to uncached memory locations that fit within a 32-bit data bus

The P6 family processors (and newer processors since) guarantee that the following
additional memory operation will always be carried out atomically:

  • Unaligned 16-, 32-, and 64-bit accesses to cached memory that fit within a cache
    line

This document also have more description of atomically for newer processors like Core2. Not all unaligned operations will be atomic.

Other intel manual recommends this white paper:

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

滥情空心 2024-10-24 20:02:47

我认为你误解了这句话。

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

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

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

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

I think you are misinterpreting the quote.

Atomicity can be guaranteed on a given architecture, using specific instructions (proper to this architecture). The MSDN article explains that read and writes on C++ built-in types can be expected to be atomic on x86 architecture.

However the C++ standard does not presume what the architecture is, therefore the Standard cannot make such guarantees. Indeed C++ is used in embedded software where the hardware support is much more limited.

C++0x defines the std::atomic template class, which allows to turn reads and writes into atomic operations, whatever the type. The compiler will select the best way to obtain atomicity based on the type characteristics and the architecture targeted in a standard compliant manner.

The new standard also defines a whole lot of operations similar to MSVC InterlockExchange that is also compiled to the fastest (yet safe) available primitives offered by the hardware.

梨涡 2024-10-24 20:02:47

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

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

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

The c++ standard does not guarantee atomic behaviour. In practice however simple load and store operations will be atomic, as the article states.

If you need atomicity, better to be explicit about it and use some sort of lock though.

*counter = 0; // this is atomic on most platforms
*counter++;   // this is NOT atomic on most platforms
泛滥成性 2024-10-24 20:02:47

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

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

Be very careful when relying on the atomicity of simple word size operations because things might behave differently from what you expect. On multicore architectures, you might witness out of order reads and writes. This will then require memory barriers to prevent. (more details here).

Bottom line for an application developer is either use primitives that the OS guarantees will be atomic or use appropriate locks.

只为一人 2024-10-24 20:02:47

IMO,本文包含了一些有关底层架构的假设。由于 C++ 对架构只有一些最低限度的要求,因此标准中无法给出原子性等保证。例如,一个字节必须至少为 8 位,但理论上,您可以拥有一个字节为 9 位、但 int 为 16 位的架构。

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

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

IMO, the article incorporates some assumptions about the underlying architecture. As C++ has only some minimalistic requirements on the architecture, no guarantees for example about atomicity can be given in the standard. For example a byte has to be at least 8 bits, but you could have an architecture where a byte is 9 bits, but an int 16... theoretically.

So when the compiler is specific for x86 architecutre, the specific features can be used.

NB: structs are usually aligned by default to a native word boundary. you can disable that by #pragma statements, so your padding fills are not required

呆头 2024-10-24 20:02:47

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

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

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

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

I think what they are trying to get accross, is that data types implemented natively by the hardware, are updated within the hardware such that reading from another thread will never give you a 'partially' updated value.

Consider a 32 bit integer on a 32+ bit machine. It is written or read completely in 1 instruction cycle, whereas data types of larger sizes, say a 64 bit int on a 32 bit machine will require more cycles, hence theoretically the thread writing them could be interrupted in between those cycles ergo the value is not in a valid state.

No useing string would not make it atomic, as string is a higher level construct and not implemented in the hardware.
Edit: As per your comment on what you (didnt) mean about changing to string, it should not make any difference to fields declared after, as mentioned in another answer the compiler will align fields by default.

The reason it is not in the standard is that, as stated in the article this is about how modern processors implement the instructions. Your standard C/C++ code should work exactly the same on a 16 or 64 bit machine (just with performance difference), however if you assume you will only execute on a 64 bit machine, then anything 64bits or smaller is atomic. (SSE etc type aside)

终陌 2024-10-24 20:02:47

我认为文章中提到的原子性几乎没有实际用途。这意味着您将读取/写入有效值,但可能已经过时。因此,读取 int 时,您将完整地读取它,而不是从旧值中读取 2 个字节,以及从当前由另一个线程写入的新值中读取另外 2 个字节。

对于共享内存来说,重要的是内存屏障。它们由同步原语(例如 C++0x 原子类型、互斥体等)保证。

I think atomicity as it is referred in the article has little practical usage. This means that you'll read/write valid value but probably outdated. So reading an int, you'll read it completely, not 2 bytes from an old value and other 2 bytes from a new value currently being written by another thread.

What is important for shared memory is memory barriers. And they are guarantied by synchronization primitives such as C++0x atomic types, mutexes etc.

_失温 2024-10-24 20:02:47

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

I do not think changing char Name[5] to std::string Name will make a difference if you are using it only for individual character assignments, since the index operator will return a direct reference to the underlying character. A full string assignment is not atomic (and you can't do it with a char array, so I'm guessing you weren't thinking of using it this way anyways).

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文