0%

内存模型基础:对象和 内存位置

书上给出了 4 点:

  1. 每个变量都是对象,包括对象中的成员。
  2. 每个对象有至少一个内存位置。
  3. 每个基本类型(int / char, …)刚好占用一个内存位置。
  4. 连续位域是同一个内存位置的一部分。

一个标量类型,或者非 0 宽的连续位域构成一个 memory location。

struct S
{
    char a;     // memory location #1
    int b : 5;  // memory location #2
    int c : 11, // memory location #2 (continued)
          : 0,
        d : 8;  // memory location #3
    struct
    {
        int ee : 8; // memory location #4
    } e;
} obj; // The object “obj” consists of 4 separate memory locations

可以参考 https://timsong-cpp.github.io/cppwp/n4868/basic.memobj ,这里有讲内存模型、对象模型、生命周期。

这一章主要讲线程之间的同步和信息传递,包括条件变量(condition variable)、futures、latches/barries。

条件变量

头文件是 <condition_variable>。包含 std::condition_variablestd::condition_variable_any。前者只能在 std::mutex 上使用,后者可以在所有满足 BasicLockable(lock() + unlock(),不需要 try_lock())的类型上使用。如果只需要使用 std::mutex,那么就用前者,开销可能会比后者小一点。

Tip

接口只有一处区别:std::condition_variable 可以获取 native handle,而 std::condition_variable_any 不能。

条件变量可以用来协调生产者和消费者之间的关系。

典型操作流程:

三种避免竞争情况的办法

  1. 上锁。
  2. 无锁编程。通常通过修改数据结构和 invariants(数据结构要保持的约束)来完成。
  3. 事务(software transactional memory, STM)。

使用互斥量保护临界区

std::lock_guard(可以用 CTAD)和 std::mutex API。C++11 除了 std::mutex 之外还有 std::timed_mutexstd::recursive_mutexstd::recursive_timed_mutex

不要把受保护数据的引用传出锁的保护范围外,尤其是要注意不要把它的引用传入到来历不明的其他函数中。

案例:实现线程安全的栈(top()pop() 的原子化)

接口设计上的问题:栈的 API 中 empty() 和其他方法各自原子是不够的,因为 empty() 的结果在下一个方法运行时不一定还能成立。

我们也可能想把 pop()top() 两个函数结合,将它们合并成一个原子化的功能,但又会遇到异常安全的问题(元素的构造函数抛出异常时,被 pop 的数据会丢失)。

2.2 向线程传递参数

向线程传递参数时最好是都复制一份,而且转换成线程启动函数期望的类型。举例:

void foo(std::string) {
  // ...
}

std::thread launch_thread() {
  char buf[1024];
  return std::thread{ foo, buf };
}

这里 foo 的参数是 std::string 类型,而 std::thread 在构造时复制的是 char * 类型的参数。等线程创建好,真正开始执行的时候,复制过来的 buf 就可能已经是悬挂引用了。

其他:想要引用可以用 std::ref,想要使用一个对象的成员函数还需要额外传递对象的地址(this 指针)。

案例:并行 accumulate 的实现

下面 parallel_accumulate 这个方法使用的是书上的朴素实现,其他测试都是调用标准库。用 Release 模式编译测试,测试在我的笔记本上进行,结果仅供参考。