内存模型

< cpp‎ | language

为 C++ 抽象机的目的定义了计算机内存存储的语义。

可为 C++ 程序所用的内存是一或多个字节的连续序列。内存中的每个字节拥有唯一的地址

字节

字节(byte)是最小的可寻址内存单元。它被定义为相接的位序列,其大到足以保有任何 UTF-8 编码单元(256 个相异值)和 (C++14 起)基本执行字符集96 个字符,要求必为单字节)的任何成员。与 C 相似,C++ 也支持 8 位或更大的字节。

charunsigned charsigned char 类型把一个字节用于存储和值表示。字节中的位数可作为 CHAR_BITstd::numeric_limits<unsigned char>::digits 访问。

内存位置

内存位置

  • 一个标量类型(算术类型、指针类型、枚举类型或 std::nullptr_t)对象
  • 或非零长位域的最大相接序列

注意:语言的各种功能特性,例如引用虚函数,可能涉及到程序不可访问,但为实现所管理的额外内存位置。

struct S {
    char a;     // 内存位置 #1
    int b : 5;  // 内存位置 #2
    int c : 11, // 内存位置 #2 (延续)
          : 0,
        d : 8;  // 内存位置 #3
    struct {
        int ee : 8; // 内存位置 #4
    } e;
} obj; // 对象 'obj' 由 4 个分离的内存位置组成

线程与数据竞争

执行线程是程序中的控制流,它始于 std::thread::threadstd::async 或以其他方式所进行的顶层函数调用。

任何线程都能潜在地访问程序中的任何对象(拥有自动或线程局部存储期的对象仍可为另一线程通过指针或引用访问)。

始终允许不同的执行线程同时访问(读和写)不同的内存位置,而无干涉或同步的任何要求。

当某个表达式的求值写入某个内存位置,而另一求值读或修改同一内存位置时,称这些表达式冲突。拥有两个冲突的求值的程序就有数据竞争,除非

  • 两个求值都在同一线程上,或同一信号处理函数中执行,或
  • 两个冲突的求值都是原子操作(见 std::atomic ),或
  • 一个冲突的求值发生早于(happens-before)另一个(见 std::memory_order

若出现数据竞争,则程序的行为未定义。

(特别是,std::mutex 的释放同步于,从而发生早于另一线程对同一 mutex 的获取,这使得可以用互斥锁来防止数据竞争)

int cnt = 0;
auto f = [&]{cnt++;};
std::thread t1{f}, t2{f}, t3{f}; // 未定义行为
std::atomic<int> cnt{0};
auto f = [&]{cnt++;};
std::thread t1{f}, t2{f}, t3{f}; // OK

内存顺序

当线程从某个内存位置读取值时,它可能看到初值,同一线程所写入的值,或另一线程所写入的值。有关线程所作的写入操作对其他线程变为可见的顺序上的细节,见 std::memory_order

向前进展

免妨碍

当只有一个未在标准库函数中阻塞的线程执行某个免锁的原子函数时,保证该执行将会完成(所有标准库免锁操作均为免妨碍的)。

免锁

当一或多个免锁原子函数同时运行时,保证其中至少一个将会完成(所有标准库免锁操作均为免锁的——确保其他线程不能不确定地活锁它们(例如以连续窃取缓存线的方式),是实现的工作)。

进展保证

合法的 C++ 程序中,每个线程最终要做下列之一:

  • 终止
  • 调用 I/O 库函数
  • 通过 volatile 泛左值进行访问
  • 进行原子操作或同步操作

没有线程能永远执行,而不做任何这些可观察行为。

注意,这意味着包含无限递归或无限循环(无论是实现为 for 语句 或是用 goto 循环还是其他方式)的程序具有未定义行为。这允许编译器移除所有无可观察行为的循环,而不必证明他们终将终止。

若线程执行了上述步骤之一(I/O、volatile、原子或同步操作),阻塞于标准库函数中,或调用由于某个未阻塞的并发线程而未能完成的原子免锁函数,则称它取得进展(make progress)

并发向前进展

若线程提供并发向前进展保证(concurrent forward progress guarantee),则只要它尚未终止,就将在有限量的时间内取得进展(定义如上),无关乎其他线程(若存在)是否取得进展。

标准鼓励但不要求主线程和 std::thread 所启动的线程提供并发向前进展保证。

并行向前进展

若线程提供并行向前进展保证(parallel forward progress guarantee),则若线程尚未执行任何执行步骤(I/O、volatile、原子或同步操作),就不要求实现保证该线程终将取得进展,但一旦此线程开始执行步骤,则它提供并发向前进展保证(此规则描述线程池中以任意顺序执行任务的线程)。

弱并行向前进展

若线程提供弱并行向前进展保证(weakly parallel forward progress guarantee),则不保证它终将取得进展,无关乎其他线程是否取得进展。

仍然能通过以向前进展保证委托进行阻塞来保证这种线程取得进展:若线程 P 以此方式阻塞于线程集合 S 的完成,则 S 中至少有一个线程将提供等于或强于 P 的向前进展保证。一旦该线程完成,则类似地强化 S 中的另一线程。一旦集合为空,则将解除 P 的阻塞。

来自 C++ 标准库的并行算法,均以向前保证委托阻塞于某个标准库所管理的线程的未指明集合的完成上。

(C++17 起)

参阅