cpp11-language-concurrency

C++11 语言扩展 — 并发

并发内存模型

内存模型是机器架构师和编译器编写者之间的一项约定,旨在确保大多数程序员不必考虑现代计算机硬件的细节。如果没有内存模型,许多与线程、锁和无锁编程相关的事情将难以理解。其关键保证是:两个执行线程可以更新和访问不同的内存位置,而不会相互干扰。但是“内存位置”是什么?内存位置可以是标量类型对象,也可以是所有位域都具有非零宽度的相邻位域的最大序列。例如,这里 `S` 恰好有四个独立的内存位置

    struct S {
        char a;         // location #1
        int b:5,        // location #2
        int c:11,
        int :0,         // note: :0 is "special"
        int d:8;        // location #3
        struct {int ee:8;} e;   // location #4
    };

为什么这很重要?为什么这不是显而易见的?这不是一直如此吗?问题是,当多个计算可以真正并行运行时,即多个(看似)不相关的指令可以同时执行时,内存硬件的怪癖就会暴露出来。事实上,在没有编译器支持的情况下,指令和数据流水线以及缓存使用的细节将以应用程序员完全无法管理的方式暴露出来。即使没有定义两个线程共享数据也是如此!考虑两个单独编译的“线程”:

    // thread 1:
    char c;
    c = 1;
    int x = c;

    // thread 2:
    char b;
    b = 1;
    int y = b;

为了更真实,我们可以使用单独编译(在每个线程内)来确保编译器/优化器不会消除内存访问,而只是忽略 `c` 和 `b` 并直接用 `1` 初始化 `x` 和 `y`。`x` 和 `y` 的可能值是什么?根据 C++11 的说法,唯一正确的答案是显而易见的:1 和 1。之所以有趣,是因为如果你使用传统的良好的、在并发之前设计的 C 或 C++ 编译器,可能的答案是 0 和 0(不太可能),1 和 0,0 和 1,以及 1 和 1。这在“实际”中已经观察到。怎么回事?链接器可能将 `c` 和 `b` 分配在紧邻的位置(在同一个字中)——C 或 C++ 1990 年代的标准对此没有异议。在那方面,C++98 类似于所有未考虑真实并发硬件而设计的语言。然而,大多数现代处理器无法读取或写入单个字符,它必须读取或写入整个字,所以对 `c` 的赋值实际上是“读取包含 `c` 的字,替换 `c` 部分,然后将字写回。”由于对 `b` 的赋值类似,所以即使线程(根据它们的源代码)不共享数据,两个线程也有很多机会相互破坏!

因此,C++11 保证“独立内存位置”不会出现此类问题。更精确地说:除非两个线程都是读取访问,否则内存位置不能在没有某种形式锁定的情况下被两个线程安全访问。请注意,单个字内的不同位域不是独立的内存位置,因此不要在线程之间共享包含位域的结构,除非有某种形式的锁定。除了这个注意事项,C++ 内存模型就“如大家所期望”的那样。

然而,在低级并发问题上保持清晰的思维并不总是那么容易。考虑

    // start with x==0 and y==0

    if (x) y = 1;   // Thread 1 

    if (y) x = 1;   // Thread 2 

这里有问题吗?更确切地说,是否存在数据竞争?(不,没有)。

幸运的是,我们已经适应了现代,并且我们知道的每一个当前 C++ 编译器都给出了一个正确答案,并且已经这样做了多年。它们对大多数(但不幸的是并非所有)棘手问题都如此。毕竟,C++ 已经用于并发系统的严肃系统编程“永远”了。标准内存模型进一步改进了这一点。

另请参见

并发中的动态初始化和销毁

参见

线程局部存储

在 C++11 中,您可以使用存储类 `thread_local` 来定义每个线程实例化一次的变量。

请注意,使用 `thread_local` 存储需要小心,并且特别是它与大多数并行算法不兼容。

参见