# 共享数据

本章节主要内容：

* 多线程共享数据的问题
* 使用互斥量保护共享数据
* 保护共享数据的其它方案
* 有关线程安全的其它问题

在上一章内容，我们对于线程的基本使用和管理，可以说已经比较了解了，甚至深入阅读了部分的 `std::thread` 源码。所以如果你好好学习了上一章，本章也完全不用担心。

我们本章，就要开始聊共享数据的那些事。

## 条件竞争

在多线程的情况下，每个线程都抢着完成自己的任务。在大多数情况下，即使会改变执行顺序，也是良性竞争，这是无所谓的。比如两个线程都要往标准输出输出一段字符，谁先谁后并不会有什么太大影响。

```cpp
void f() { std::cout << "❤️\n"; }
void f2() { std::cout << "😢\n"; }

int main(){
    std::thread t{ f };
    std::thread t2{ f2 };
    t.join();
    t2.join();
}
```

> [`std::cout`](https://zh.cppreference.com/w/cpp/io/cout) 的 operator<< 调用是线程安全的，不会被打断。即：*同步的 C++ 流保证是线程安全的（从多个线程输出的单独字符可能交错，但无数据竞争）*

只有在涉及多线程读写相同共享数据的时候，才会导致“*恶性的条件竞争*”。

```cpp
std::vector<int>v;

void f() { v.emplace_back(1); }
void f2() { v.erase(v.begin()); }

int main() {
    std::thread t{ f };
    std::thread t2{ f2 };
    t.join();
    t2.join();
    std::cout << v.size() << '\n';
}
```

比如这段代码就是典型的恶性条件竞争，两个线程共享一个 `vector`，并对它进行修改。可能导致许多问题，比如 `f2` 先执行，此时 `vector` 还没有元素，导致抛出异常。又或者 `f` 执行了一半，调用了 `f2()`，等等。

当然了，也有可能先执行 f，然后执行 f2，最后打印了 0，程序老老实实执行完毕。

但是我们显然不能寄希望于这种操作系统的调度。

而且即使不是一个添加元素，一个删除元素，全是 `emplace_back` 添加元素，也一样会有问题，由于 std::vector 不是线程安全的容器，因此当多个线程同时访问并修改 v 时，可能会发生[*未定义的行为*](https://zh.cppreference.com/w/cpp/language/memory_model#.E7.BA.BF.E7.A8.8B.E4.B8.8E.E6.95.B0.E6.8D.AE.E7.AB.9E.E4.BA.89)。具体来说，当两个线程同时尝试向 v 中添加元素时，但是 `emplace_back` 函数却是可以被打断的，执行了一半，又去执行另一个线程。可能会导致数据竞争，从而引发*未定义*的结果。

> 当某个表达式的求值写入某个内存位置，而另一求值读或修改同一内存位置时，称这些**表达式冲突**。**拥有两个冲突的求值的程序就有数据竞争**，除非
>
> * 两个求值都在同一线程上，或者在同一信号处理函数中执行，或
> * 两个冲突的求值都是原子操作（见 std::atomic），或
> * 一个冲突的求值发生早于 另一个（见 std::memory\_order）
>
> **如果出现数据竞争，那么程序的行为未定义。**

标量类型等都同理，有*数据竞争*，[*未定义行为*](https://zh.cppreference.com/w/cpp/language/memory_model#.E7.BA.BF.E7.A8.8B.E4.B8.8E.E6.95.B0.E6.8D.AE.E7.AB.9E.E4.BA.89)：

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

## 使用互斥量

互斥量（Mutex），又常被称为互斥锁、互斥体（或者直接被称作“锁”），是一种用来保护**临界区**的特殊对象，其相当于实现了一个公共的“**标志位**”。它可以处于锁定（locked）状态，也可以处于解锁（unlocked）状态：

1. 如果互斥量是锁定的，通常说某个特定的线程正持有这个锁。
2. 如果没有线程持有这个互斥量，那么这个互斥量就处于解锁状态。

***

概念从来不是我们的重点，用一段对比代码为你直观的展示互斥量的作用：

```cpp
void f() {
    std::cout << std::this_thread::get_id() << '\n';
}

int main() {
    std::vector<std::thread> threads;
    for (std::size_t i = 0; i < 10; ++i)
        threads.emplace_back(f);

    for (auto& thread : threads)
        thread.join();
}
```

这段代码你多次[运行](https://godbolt.org/z/93vTzcPzK)它会得到毫无规律和格式的结果，我们可以使用[互斥量](https://zh.cppreference.com/w/cpp/thread/mutex)解决这个问题：

```cpp
#include <mutex> // 必要标头
std::mutex m;

void f() {
    m.lock();
    std::cout << std::this_thread::get_id() << '\n';
    m.unlock();
}

int main() {
    std::vector<std::thread>threads;
    for (std::size_t i = 0; i < 10; ++i)
        threads.emplace_back(f);

    for (auto& thread : threads)
        thread.join();
}
```

当多个线程执行函数 `f` 的时候，只有一个线程能成功调用 `lock()` 给互斥量上锁，其他所有的线程 `lock()` 的调用将阻塞执行，直至获得锁。第一个调用 `lock()` 的线程得以继续往下执行，执行我们的 `std::cout` 输出语句，不会有任何其他的线程打断这个操作。直到线程执行 `unlock()`，就解锁了互斥量。

那么其他线程此时也就能再有一个成功调用 `lock`...

> 至于到底哪个线程才会成功调用，这个是由操作系统调度决定的。

看一遍描述就可以了，简而言之，被 `lock()` 和 `unlock()` 包含在其中的代码是线程安全的，同一时间只有一个线程执行，不会被其它线程的执行所打断。

### `std::lock_guard`

不过一般不推荐这样显式的 `lock()` 与 `unlock()`，我们可以使用 C++11 标准库引入的“管理类” [`std::lock_guard`](https://zh.cppreference.com/w/cpp/thread/lock_guard)：

```cpp
void f() {
    std::lock_guard<std::mutex> lc{ m };
    std::cout << std::this_thread::get_id() << '\n';
}
```

那么问题来了，`std::lock_guard` 是如何做到的呢？它是怎么实现的呢？首先顾名思义，这是一个“管理类”模板，用来管理互斥量的上锁与解锁，我们来看它在 [MSVC STL](https://github.com/microsoft/STL/blob/8dc4faadafb52e3e0a627e046b41258032d9bc6a/stl/inc/mutex#L448-L469) 的实现：

```cpp
_EXPORT_STD template <class _Mutex>
class _NODISCARD_LOCK lock_guard { // class with destructor that unlocks a mutex
public:
    using mutex_type = _Mutex;

    explicit lock_guard(_Mutex& _Mtx) : _MyMutex(_Mtx) { // construct and lock
        _MyMutex.lock();
    }

    lock_guard(_Mutex& _Mtx, adopt_lock_t) noexcept // strengthened
        : _MyMutex(_Mtx) {} // construct but don't lock

    ~lock_guard() noexcept {
        _MyMutex.unlock();
    }

    lock_guard(const lock_guard&)            = delete;
    lock_guard& operator=(const lock_guard&) = delete;

private:
    _Mutex& _MyMutex;
};
```

这段代码极其简单，首先管理类，自然不可移动不可复制，我们定义复制构造与复制赋值为[弃置函数](https://zh.cppreference.com/w/cpp/language/function#.E5.BC.83.E7.BD.AE.E5.87.BD.E6.95.B0)，同时[阻止](https://zh.cppreference.com/w/cpp/language/rule_of_three#.E4.BA.94.E4.B9.8B.E6.B3.95.E5.88.99)了移动等函数的隐式定义。

它只保有一个私有数据成员，一个引用，用来引用互斥量。

构造函数中初始化这个引用，同时上锁，析构函数中解锁，这是一个非常典型的 `RAII` 式的管理。

同时它还提供一个有额外[`std::adopt_lock_t`](https://zh.cppreference.com/w/cpp/thread/lock_tag_t)参数的构造函数 ，如果使用这个构造函数，则构造函数不会上锁。

所以有的时候你可能会看到一些这样的代码：

```cpp
void f(){
    //code..
    {
        std::lock_guard<std::mutex> lc{ m };
        // 涉及共享资源的修改的代码...
    }
    //code..
}
```

使用 `{}` 创建了一个块作用域，限制了对象 `lc` 的生存期，进入作用域构造 `lock_guard` 的时候上锁（lock），离开作用域析构的时候解锁（unlock）。

* 我们要尽可能的让互斥量上锁的**粒度**小，只用来确保必须的共享资源的线程安全。

> **“粒度”通常用于描述锁定的范围大小，较小的粒度意味着锁定的范围更小，因此有更好的性能和更少的竞争。**

我们举一个例子：

```cpp
std::mutex m;

void add_to_list(int n, std::list<int>& list) {
    std::vector<int> numbers(n + 1);
    std::iota(numbers.begin(), numbers.end(), 0);
    int sum = std::accumulate(numbers.begin(), numbers.end(), 0);

    {
        std::lock_guard<std::mutex> lc{ m };
        list.push_back(sum);
    }
}
void print_list(const std::list<int>& list){
    std::lock_guard<std::mutex> lc{ m };
    for(const auto& i : list){
        std::cout << i << ' ';
    }
    std::cout << '\n';
}
```

```cpp
std::list<int> list;
std::thread t1{ add_to_list,i,std::ref(list) };
std::thread t2{ add_to_list,i,std::ref(list) };
std::thread t3{ print_list,std::cref(list) };
std::thread t4{ print_list,std::cref(list) };
t1.join();
t2.join();
t3.join();
t4.join();
```

> 完整[代码测试](https://godbolt.org/z/E3396dMxG)。

先看 `add_to_list`，只有 `list.push_back(sum)` 涉及到了对共享数据的修改，需要进行保护，我们用 `{}` 包起来了。

假设有线程 A、B执行函数 `add_to_list()` ：线程 A 中的 numbers、sum 与线程 B 中的，不是同一个，希望大家分清楚，自然不存在数据竞争，也不需要上锁。线程 A、B执行完了前面求 `0-n` 的计算，只有一个线程能在 `lock_guard` 的构造函数中成功调用 lock() 给互斥量上锁。假设线程 A 成功调用 lock()，那么线程 B 的 lock() 调用就阻塞了，必须等待线程 A 执行完里面的代码，然后作用域结束，调用 `lock_guard` 的析构函数，解锁 unlock()，此时线程 B 就可以进去执行了，避免了数据竞争，不存在一个对象同时被多个线程修改。

函数 `print_list()` 就更简单了，打印 `list`，给整个函数上锁，同一时刻只能有一个线程执行。

我们的使用代码是多个线程执行这两个函数，两个函数共享了一个锁，这样确保了当执行函数 `print_list()` 打印的时候，list 的状态是确定的。打印函数 `print_list` 和 `add_to_list` 函数的修改操作同一时间只能有一个线程在执行。`print_list()` 不可能看到正在被`add_to_list()` 修改的 list。

至于到底哪个函数哪个线程会先执行，执行多少次，这些都由操作系统调度决定，也完全有可能**连续 4 次**都是执行函数 `print_list` 的线程成功调用 `lock`，会打印出了一样的值，这都很正常。

***

C++17 添加了一个新的特性，[类模板实参推导](https://zh.cppreference.com/w/cpp/language/class_template_argument_deduction)， `std::lock_guard` 可以根据传入的参数自行推导，而不需要写明模板类型参数：

```cpp
std::mutex m;
std::lock_guard lc{ m }; // std::lock_guard<std::mutex>
```

并且 C++17 还引入了一个新的“管理类”：[`std::scoped_lock`](https://zh.cppreference.com/w/cpp/thread/scoped_lock)，它相较于 `lock_guard`的区别在于，**它可以管理多个互斥量**。不过对于处理一个互斥量的情况，它和 `lock_guard` 几乎完全相同。

```cpp
std::mutex m;
std::scoped_lock lc{ m }; // std::scoped_lock<std::mutex>
```

我们在后续管理多个互斥量，会详细了解这个类。

### `try_lock`

`try_lock` 是互斥量中的一种尝试上锁的方式。与常规的 `lock` 不同，`try_lock` 会尝试上锁，但如果锁已经被其他线程占用，则**不会阻塞当前线程，而是立即返回**。

它的返回类型是 `bool` ，如果上锁成功就返回 `true`，失败就返回 `false`。

这种方法在多线程编程中很有用，特别是在需要保护临界区的同时，又不想线程因为等待锁而阻塞的情况下。

```cpp
std::mutex mtx;

void thread_function(int id) {
    // 尝试加锁
    if (mtx.try_lock()) {
        std::cout << "线程：" << id << " 获得锁" << std::endl;
        // 临界区代码
        std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟临界区操作
        mtx.unlock(); // 解锁
        std::cout << "线程：" << id << " 释放锁" << std::endl;
    } else {
        std::cout << "线程：" << id << " 获取锁失败 处理步骤" << std::endl;
    }
}
```

如果有两个线程运行这段代码，必然有一个线程无法成功上锁，要走 else 的分支。

```cpp
std::thread t1(thread_function, 1);
std::thread t2(thread_function, 2);

t1.join();
t2.join();
```

可能的[**运行结果**](https://godbolt.org/z/hqW8eahWx)：

```txt
线程：1 获得锁
线程：2 获取锁失败 处理步骤
线程：1 释放锁
```

## 保护共享数据

互斥量主要也就是为了保护共享数据，上一节的*使用互斥量*也已经为各位展示了一些。

然而使用互斥量来保护共享数据也并不是在函数中加上一个 `std::lock_guard` 就万事大吉了。有的时候只需要一个指针或者引用，就能让这种保护**形同虚设**。

```cpp
class Data{
    int a{};
    std::string b{};
public:
    void do_something(){
        // 修改数据成员等...
    }
};

class Data_wrapper{
    Data data;
    std::mutex m;
public:
    template<class Func>
    void process_data(Func func){
        std::lock_guard<std::mutex> lc{m};
        func(data);  // 受保护数据传递给函数
    }
};

Data* p = nullptr;

void malicious_function(Data& protected_data){
    p = &protected_data; // 受保护的数据被传递到外部
}

Data_wrapper d;

void foo(){
    d.process_data(malicious_function);  // 传递了一个恶意的函数
    p->do_something();                   // 在无保护的情况下访问保护数据
}
```

成员函数模板 `process_data` 看起来一点问题也没有，使用 `std::lock_guard` 对数据做了保护，但是调用方传递了 `malicious_function` 这样一个恶意的函数，使受保护数据传递给外部，可以在没有被互斥量保护的情况下调用 `do_something()`。

我们传递的函数就不该是涉及外部副作用的，就应该是单纯的在受互斥量保护的情况下老老实实调用 `do_something()` 操作受保护的数据。

* *简而言之：**切勿将受保护数据的指针或引用传递到互斥量作用域之外**，不然保护将**形同虚设**。*

> `process_data` 的确算是没问题，用户非要做这些事情也是防不住的，我们只是告诉各位可能的情况。

## 死锁：问题与解决

试想一下，有一个玩具，这个玩具有两个部分，必须同时拿到两部分才能玩。比如一个遥控汽车，需要遥控器和玩具车才能玩。有两个小孩，他们都想玩这个玩具。当其中一个小孩拿到了遥控器和玩具车时，就可以尽情玩耍。当另一个小孩也想玩，他就得等待另一个小孩玩完才行。再试想，遥控器和玩具车被放在两个不同的地方，并且两个小孩都想要玩，并且一个拿到了遥控器，另一个拿到了玩具车。问题就出现了，除非其中一个孩子决定让另一个先玩，他把自己的那个部分给另一个小孩。但如果他们都不愿意，那么这个遥控汽车就谁都没有办法玩。

我们当然不在乎小孩抢玩具，我们要聊的是线程对锁的竞争：*两个线程需要对它们所有的互斥量做一些操作，其中每个线程都有一个互斥量，且等待另一个线程的互斥量解锁。因为它们都在等待对方释放互斥量，没有线程工作。* 这种情况就是死锁。

* **多个互斥量才可能遇到死锁问题**。

避免死锁的一般建议是让两个互斥量以相同的顺序上锁，总在互斥量 B 之前锁住互斥量 A，就通常不会死锁。反面示例

```cpp
std::mutex m1,m2;
std::size_t n{};

void f(){
    std::lock_guard<std::mutex> lc1{ m1 };
    std::lock_guard<std::mutex> lc2{ m2 };
    ++n;
}
void f2() {
    std::lock_guard<std::mutex> lc1{ m2 };
    std::lock_guard<std::mutex> lc2{ m1 };
    ++n;
}
```

`f` 与 `f2` 因为互斥量**上锁顺序不同**，就有死锁风险。函数 `f` 先锁定 `m1`，然后再尝试锁定 `m2`，而函数 `f2` 先锁定 `m2` 再锁定 `m1` 。如果两个线程同时运行，它们就可能会彼此等待对方释放其所需的锁，从而造成死锁。

> 简而言之，有可能函数 f 锁定了 m1，函数 f2 锁定了 m2，函数 f 要往下执行，给 m2 上锁，所以在等待 f2 解锁 m2，然而函数 f2 也在等待函数 f 解锁 m1 它才能往下执行。所以死锁。[测试代码](https://godbolt.org/z/T8vWYzqnT)。

***

但是有的时候即使固定锁顺序，依旧会产生问题。当有多个互斥量保护同一个类的对象时，对于相同类型的两个不同对象进行数据的交换操作，为了保证数据交换的正确性，就要避免其它线程修改，确保每个对象的互斥量都锁住自己要保护的区域。如果按照前面的的选择一个固定的顺序上锁解锁，则毫无意义，比如：

```cpp
struct X{
    X(const std::string& str) :object{ str } {}

    friend void swap(X& lhs, X& rhs);
private:
    std::string object;
    std::mutex m;
};

void swap(X& lhs, X& rhs) {
    if (&lhs == &rhs) return;
    std::lock_guard<std::mutex> lock1{ lhs.m }; 
    std::lock_guard<std::mutex> lock2{ rhs.m }; 
    swap(lhs.object, rhs.object);
}
```

**考虑用户调用的时候将参数交换，就会产生死锁**：

```cpp
X a{ "🤣" }, b{ "😅" };
std::thread t{ [&] {swap(a, b); } };  // 1
std::thread t2{ [&] {swap(b, a); } }; // 2
```

`1` 执行的时候，先上锁 a 的互斥量，再上锁 b 的互斥量。

`2` 执行的时候，先上锁 b 的互斥量，再上锁 a 的互斥量。

> 完全可能线程 A 执行 1 的时候上锁了 a 的互斥量，线程 B 执行 `2` 上锁了 b 的互斥量。线程 A 往下执行需要上锁 b 的互斥量，线程 B 则要上锁 a 的互斥量执行完毕才能解锁，哪个都没办法往下执行，**死锁**。[测试代码](https://godbolt.org/z/M6hfeb778)。

其实也就是回到了第一个示例的问题。

C++ 标准库有很多办法解决这个问题，**可以使用** [**`std::lock`**](https://zh.cppreference.com/w/cpp/thread/lock) ，它能一次性锁住多个互斥量，并且没有死锁风险。修改 swap 代码后如下：

```cpp
void swap(X& lhs, X& rhs) {
    if (&lhs == &rhs) return;
    std::lock(lhs.m, rhs.m);    // 给两个互斥量上锁
    std::lock_guard<std::mutex> lock1{ lhs.m,std::adopt_lock }; 
    std::lock_guard<std::mutex> lock2{ rhs.m,std::adopt_lock }; 
    swap(lhs.object, rhs.object);
}
```

因为前面已经使用了 `std::lock` 上锁，所以后的 `std::lock_guard` 构造都额外传递了一个 `std::adopt_lock` 参数，让其选择到不上锁的构造函数。函数退出也能正常解锁。

`std::lock` 给 `lhs.m` 或 `rhs.m` 上锁时若抛出异常，则在重抛前对任何已锁的对象调用 `unlock()` 解锁，也就是 `std::lock` 要么将互斥量都上锁，要么一个都不锁。

C++17 新增了 [`std::scoped_lock`](https://zh.cppreference.com/w/cpp/thread/scoped_lock) ，提供此函数的 [RAII](https://zh.cppreference.com/w/cpp/language/raii) 包装，通常它比裸调用 `std::lock` 更好。

所以我们前面的代码可以改写为：

```cpp
void swap(X& lhs, X& rhs) {
    if (&lhs == &rhs) return;
    std::scoped_lock guard{ lhs.m,rhs.m };
    swap(lhs.object, rhs.object);
}
```

对此类有兴趣或任何疑问，建议阅读[**`std::scoped_lock` 的源码实现与解析**](https://mq-bai.gitbook.io/modern-cpp-concurrent-programming/md/xiang-xi-fen-xi/02scopedlock-yuan-ma-jie-xi)

使用 `std::scoped_lock` 可以将所有 `std::lock` 替换掉，减少错误发生。

然而它们的帮助都是有限的，一切最终都是依靠开发者使用与管理。

死锁是多线程编程中令人相当头疼的问题，并且死锁经常是不可预见，甚至难以复现，因为在大部分时间里，程序都能正常完成工作。我们可以**通过一些简单的规则，约束开发者的行为，帮助写出“无死锁”的代码**。

* **避免嵌套锁**

  线程获取一个锁时，就别再获取第二个锁。每个线程只持有一个锁，自然不会产生死锁。如果必须要获取多个锁，使用 `std::lock` 。
* **避免在持有锁时调用外部代码**

  这个建议是很简单的：因为代码是外部提供的，所以没办法确定外部要做什么。外部程序可能做任何事情，包括获取锁。在持有锁的情况下，如果用外部代码要获取一个锁，就会违反第一个指导意见，并造成死锁（有时这是无法避免的）。当写通用代码时（比如[保护共享数据](#保护共享数据)中的 `Date` 类）。这不是接口设计者可以处理的，只能寄希望于调用方传递的代码是能正常执行的。
* **使用固定顺序获取锁**

  如同第一个示例那样，固定的顺序上锁就不存在问题。

## `std::unique_lock` 灵活的锁

[`std::unique_lock`](https://zh.cppreference.com/w/cpp/thread/unique_lock) 是 C++11 引入的一种通用互斥包装器，它相比于 `std::lock_guard` 更加的灵活。当然，它也更加的复杂，尤其它还可以与我们下一章要讲的[条件变量](https://zh.cppreference.com/w/cpp/thread#.E6.9D.A1.E4.BB.B6.E5.8F.98.E9.87.8F)一起使用。使用它可以将之前使用 `std::lock_guard` 的 `swap` 改写一下：

```cpp
void swap(X& lhs, X& rhs) {
    if (&lhs == &rhs) return;
    std::unique_lock<std::mutex> lock1{ lhs.m, std::defer_lock };
    std::unique_lock<std::mutex> lock2{ rhs.m, std::defer_lock };
    std::lock(lock1, lock2);
    swap(lhs.object, rhs.object);
}
```

解释这段代码最简单的方式就是直接展示标准库的源码，首先，我们要了解 `std::defer_lock` 是“不获得互斥体的所有权”。没有所有权自然构造函数就不会上锁，但不止如此。我们还要先知道 std::unique\_lock 保有的数据成员（都以 [MSVC STL](https://github.com/microsoft/STL/blob/8dc4faadafb52e3e0a627e046b41258032d9bc6a/stl/inc/mutex#L135-L269) 为例）：

```cpp
private:
    _Mutex* _Pmtx = nullptr;
    bool _Owns    = false;
```

如你所见很简单，一个互斥量的指针，还有一个就是表示对象是否拥有互斥量所有权的 bool 类型的对象 `_Owns` 了。我们前面代码会调用构造函数：

```cpp
unique_lock(_Mutex& _Mtx, defer_lock_t) noexcept
    : _Pmtx(_STD addressof(_Mtx)), _Owns(false) {} // construct but don't lock
```

如你所见，只是初始化了数据成员而已，注意，**这个构造函数没有给互斥量上锁，且 `_Owns` 为 `false` 表示没有互斥量所有权**。并且 `std::unique_lock` 是有 [`lock()`](https://zh.cppreference.com/w/cpp/thread/unique_lock/lock) 、[`try_lock()`](https://zh.cppreference.com/w/cpp/thread/unique_lock/try_lock) 、[`unlock()`](https://zh.cppreference.com/w/cpp/thread/unique_lock/unlock) 成员函数的，所以可以直接传递给 `std::lock`、 进行调用。这里还需要提一下 `lock()` 成员函数的代码：

```cpp
void lock() { // lock the mutex
    _Validate();
    _Pmtx->lock();
    _Owns = true;
}
```

如你所见，正常上锁，并且把 `_Owns` 设置为 `true`，即表示当前对象拥有互斥量的所有权。那么接下来看析构函数：

```cpp
~unique_lock() noexcept {
    if (_Owns) {
        _Pmtx->unlock();
    }
}
```

必须得是**当前对象拥有互斥量的所有权析构函数才会调用 unlock() 解锁互斥量**。我们的代码因为调用了 `lock` ，所以 `_Owns` 设置为 `true` ，函数结束对象析构的时候会解锁互斥量。

***

设计挺奇怪的对吧，这个所有权语义。其实上面的代码还不够简单直接，我们再举个例子：

```cpp
std::mutex m;

int main() {
    std::unique_lock<std::mutex> lock{ m,std::adopt_lock };
    lock.lock();
}
```

这段代码运行会[抛出异常](https://godbolt.org/z/r44nWeb16)，原因很简单，因为 `std::adopt_lock` 只是不上锁，但是**有所有权**，即 `_Owns` 设置为 `true` 了，当运行 `lock()` 成员函数的时候，调用了 `_Validate()` 进行检测，也就是：

```cpp
void _Validate() const { // check if the mutex can be locked
    if (!_Pmtx) {
        _Throw_system_error(errc::operation_not_permitted);
    }

    if (_Owns) {
        _Throw_system_error(errc::resource_deadlock_would_occur);
    }
}
```

满足第二个 if，因为 `_Owns` 为 `true` 所以抛出异常，别的标准库也都有[类似设计](https://github.com/gcc-mirror/gcc/blob/3e3d115c946944c81d8231dfbe778d4dae26cbb7/libstdc%2B%2B-v3/include/bits/unique_lock.h#L141-L144)。很诡异的设计对吧，正常。除非我们写成：

```cpp
lock.mutex()->lock();
```

也就是说 `std::unique_lock` 要想调用 `lock()` 成员函数，必须是当前**没有所有权**。

所以正常的用法其实是，先上锁了互斥量，然后传递 `std::adopt_lock` 构造 `std::unique_lock` 对象表示拥有互斥量的所有权，即可在析构的时候正常解锁。如下：

```cpp
std::mutex m;

int main() {
    m.lock();
    std::unique_lock<std::mutex> lock { m,std::adopt_lock };
}
```

***

简而言之：

* 使用 `std::defer_lock` 构造函数不上锁，要求构造之后上锁
* 使用 `std::adopt_lock` 构造函数不上锁，要求在构造之前互斥量上锁
* 默认构造会上锁，要求构造函数之前和构造函数之后都不能再次上锁

***

我们前面提到了 `std::unique_lock` 更加灵活，那么灵活在哪？很简单，它拥有 `lock()` 和 `unlock()` 成员函数，所以我们能写出如下代码：

```cpp
void f() {
    //code..
    
    std::unique_lock<std::mutex> lock{ m };

    // 涉及共享资源的修改的代码...

    lock.unlock(); // 解锁并释放所有权，析构函数不会再 unlock()

    //code..
}
```

而不是像之前 `std::lock_guard` 一样使用 `{}`。

另外再聊一聊开销吧，其实倒也还好，多了一个 `bool` ，内存对齐，x64 环境也就是 `16` 字节。这都不是最重要的，主要是复杂性和需求，通常建议优先 `std::lock_guard`，当它无法满足你的需求或者显得代码非常繁琐，那么可以考虑使用 `std::unique_lock`。

## 在不同作用域传递互斥量

首先我们要明白，互斥量满足[*互斥体 (Mutex)*](https://zh.cppreference.com/w/cpp/named_req/Mutex)的要求，**不可复制不可移动**。所谓的在不同作用域传递互斥量，其实只是传递了它们的指针或者引用罢了。可以利用各种类来进行传递，比如前面提到的 `std::unique_lock`。

`std::unique_lock` 可以获取互斥量的所有权，而互斥量的所有权可以通过移动操作转移给其他的 `std::unique_lock` 对象。有些时候，这种转移（*就是调用移动构造*）是自动发生的，比如当[函数返回](https://zh.cppreference.com/w/cpp/language/return#.E8.87.AA.E5.8A.A8.E4.BB.8E.E5.B1.80.E9.83.A8.E5.8F.98.E9.87.8F.E5.92.8C.E5.BD.A2.E5.8F.82.E7.A7.BB.E5.8A.A8) `std::unique_lock` 对象。另一种情况就是得显式使用 [`std::move`](https://zh.cppreference.com/w/cpp/utility/move)。

> 请勿对移动语义和转移所有权抱有错误的幻想，我们说的无非是调用 `std::unique_lock` 的移动构造罢了：
>
> ```cpp
> _NODISCARD_CTOR_LOCK unique_lock(unique_lock&& _Other) noexcept : _Pmtx(_Other._Pmtx), _Owns(_Other._Owns) {
>     _Other._Pmtx = nullptr;
>     _Other._Owns = false;
> }
> ```
>
> 将数据成员赋给新对象，原来的置空，这就是所谓的 *“**所有权**”转移*，切勿被词语迷惑。

`std::unique_lock` 是只能移动不可复制的类，它移动即标志其管理的互斥量的所有权转移了。

一种可能的使用是允许函数去锁住一个互斥量，并将互斥量的所有权转移到调用者上，所以调用者可以在这个锁保护的范围内执行代码。

```cpp
std::unique_lock<std::mutex> get_lock(){
    extern std::mutex some_mutex;
    std::unique_lock<std::mutex> lk{ some_mutex };
    return lk;
}
void process_data(){
    std::unique_lock<std::mutex> lk{ get_lock() };
    // 执行一些任务...
}
```

`return lk` 这里会调用移动构造，将互斥量的所有权转移给调用方， `process_data` 函数结束的时候会解锁互斥量。

我相信你可能对 `extern std::mutex some_mutex` 有疑问，其实不用感到奇怪，这是一个互斥量的声明，可能别的翻译单元（或 dll 等）有它的定义，成功链接上。我们前面也说了：“*所谓的在不同作用域传递互斥量，其实只是传递了它们的指针或者引用罢了*”，所以**要特别注意互斥量的**[**生存期**](https://zh.cppreference.com/w/cpp/language/lifetime)。

> extern 说明符只能搭配变量声明和函数声明（除了类成员或函数形参）。*它指定外部链接，而且技术上不影响存储期，但它不能用来定义自动存储期的对象，故所有 extern 对象都具有**静态或线程***[***存储期***](https://zh.cppreference.com/w/cpp/language/storage_duration)***。***

如果你简单写一个 `std::mutex some_mutex` 那么函数 `process_data` 中的 `lk` 会持有一个悬垂指针。

> 举一个使用 `extern std::mutex` 的完整[运行示例](https://godbolt.org/z/z47x1Es5z)。当然，其实理论上你 `new std::mutex` 也是完全可行...... 🤣🤣

`std::unique_lock` 是灵活的，同样允许在对象销毁之前就解锁互斥量，调用 `unlock()` 成员函数即可，不再强调。

## 保护共享数据的初始化过程

保护共享数据并非必须使用互斥量，互斥量只是其中一种常见的方式而已，对于一些特殊的场景，也有专门的保护方式，比如**对于共享数据的初始化过程的保护**。我们通常就不会用互斥量，**这会造成很多的额外开销**。

我们不想为各位介绍其它乱七八糟的各种保护初始化的方式，我们只介绍三种：**双检锁（错误）**、**使用 `std::call_once`**、**静态局部变量初始化从 C++11 开始是线程安全**。

1. **双检锁（错误）线程不安全**

   ```cpp
   void f(){
       if(!ptr){      // 1
           std::lock_guard<std::mutex> lk{ m };
           if(!ptr){  // 2
               ptr.reset(new some);  // 3
           }
       }
       ptr->do_something();  // 4
   }
   ```

   ① 是查看指针是否为空，空才需要初始化，才需要获取锁。指针为空，当获取锁后会再检查一次指针②（这就是双重检查），避免另一线程在第一次检查后再做初始化，并且让当前线程获取锁。

   然而这显然没用，因为**有潜在的条件竞争**。未被锁保护的读取操作①没有与其他线程里被锁保护的写入操作③进行同步，因此就会产生条件竞争。

   简而言之：一个线程知道另一个线程已经在执行③，但是此时还没有创建 some 对象，而只是分配内存对指针写入。那么这个线程在①的时候就不会进入，直接执行了 `ptr->do_something()`④，得不到正确的结果，因为对象还没构造。

   > 如果你觉得难以理解，那就记住 `ptr.reset(new some);` 并非是不可打断不可交换的固定指令。
   >
   > 这种错误写法在一些单例中也非常的常见。如果你的同事或上司写出此代码，一般不建议指出，因为不见得你能教会他们，不要“没事找事”，只要不影响自己即可。
2. C++ 标准委员会也认为处理此问题很重要，**所以标准库提供了** [**`std::call_once`**](https://zh.cppreference.com/w/cpp/thread/call_once) **和** [**`std::once_flag`**](https://zh.cppreference.com/w/cpp/thread/once_flag) 来处理这种情况。比起锁住互斥量并显式检查指针，每个线程只需要使用 `std::call_once` 就可以。**使用 `std::call_once` 比显式使用互斥量消耗的资源更少，特别是当初始化完成之后**。

   ```cpp
   std::shared_ptr<some> ptr;
   std::once_flag resource_flag;

   void init_resource(){
       ptr.reset(new some);
   }

   void foo(){
       std::call_once(resource_flag, init_resource); // 线程安全的一次初始化
       ptr->do_something();
   }
   ```

   以上代码 `std::once_flag` 对象是全局命名空间作用域声明，如果你有需要，它也可以是类的成员。用于搭配 `std::call_once` 使用，保证线程安全的**一次**初始化。`std::call_once` 只需要接受[*可调用 (Callable)*](https://zh.cppreference.com/w/cpp/named_req/Callable)对象即可，也不要求一定是函数。

   > “**初始化**”，那自然是只有**一次**。但是 `std::call_once` 也有一些例外情况（比如异常）会让传入的可调用对象被多次调用，即“**多次**”初始化：
   >
   > ```cpp
   > std::once_flag flag;
   > int n = 0;
   >
   > void f(){
   >     std::call_once(flag, [] {
   >         ++n;
   >         std::cout << "第" << n << "次调用\n";
   >         throw std::runtime_error("异常");
   >     });
   > }
   >
   > int main(){
   >     try{
   >         f();
   >     }
   >     catch (std::exception&){}
   >     
   >     try{
   >         f();
   >     }
   >     catch (std::exception&){}
   > }
   > ```
   >
   > [测试链接](https://godbolt.org/z/aWqfEchd6)。正常情况会保证传入的可调用对象只调用一次，即初始化只有一次。异常之类的是例外。
   >
   > 这种行为很合理，因为异常代表操作失败，需要进行回溯和重置状态，符合语义和设计。
3. **静态局部变量初始化在 C++11 是线程安全**

   ```cpp
   class my_class;
   inline my_class& get_my_class_instance(){
       static my_class instance;  // 线程安全的初始化过程 初始化严格发生一次
       return instance;
   }
   ```

   即使多个线程同时访问 `get_my_class_instance` 函数，也只有一个线程会执行 instance 的初始化，其它线程会等待初始化完成。这种实现方式是线程安全的，不用担心数据竞争。此方式也在单例中多见，被称作“Meyers Singleton”单例，是简单合理的做法。

***

其实还有不少其他的做法或者反例，但是觉得没必要再聊了，这些已经足够了，再多下去就冗余了。且本文不是详尽的文档，而是“**教程**”。

## 保护不常更新的数据结构

试想一下，你有一个数据结构存储了用户的设置信息，每次用户打开程序的时候，都要进行读取，且运行时很多地方都依赖这个数据结构需要读取，所以为了效率，我们使用了多线程读写。这个数据结构很少进行改变，而我们知道，多线程读取，是没有数据竞争的，是安全的，但是有些时候又不可避免的有修改和读取都要工作的时候，所以依然必须得使用互斥量进行保护。

然而使用 `std::mutex` 的开销是过大的，它不管有没有发生数据竞争（也就是就算全是读的情况）也必须是老老实实上锁解锁，只有一个线程可以运行。如果你学过其它语言或者操作系统，相信这个时候就已经想到了：“[***读写锁***](https://zh.wikipedia.org/wiki/%E8%AF%BB%E5%86%99%E9%94%81)”。

C++ 标准库自然为我们提供了： [`std::shared_timed_mutex`](https://zh.cppreference.com/w/cpp/thread/shared_timed_mutex)（C++14）、 [`std::shared_mutex`](https://zh.cppreference.com/w/cpp/thread/shared_mutex)（C++17）。它们的区别简单来说，前者支持更多的操作方式，后者有更高的性能优势。

`std::shared_mutex` 同样支持 `std::lock_guard`、`std::unique_lock`。和 `std::mutex` 做的一样，保证*写线程*的独占访问。**而那些无需修改数据结构的*****读线程*****，可以使用** [**`std::shared_lock<std::shared_mutex>`**](https://zh.cppreference.com/w/cpp/thread/shared_lock) **获取访问权**，多个线程可以一起读取。

```cpp
class Settings {
private:
    std::map<std::string, std::string> data_;
    mutable std::shared_mutex mutex_; // “M&M 规则”：mutable 与 mutex 一起出现

public:
    void set(const std::string& key, const std::string& value) {
        std::lock_guard<std::shared_mutex> lock{ mutex_ };
        data_[key] = value;
    }

    std::string get(const std::string& key) const {
        std::shared_lock<std::shared_mutex> lock(mutex_);
        auto it = data_.find(key);
        return (it != data_.end()) ? it->second : ""; // 如果没有找到键返回空字符串
    }
};
```

> [完整代码](https://github.com/Mq-b/ModernCpp-ConcurrentProgramming-Tutorial/blob/main/code/03%E5%85%B1%E4%BA%AB%E6%95%B0%E6%8D%AE/%E4%BF%9D%E6%8A%A4%E4%B8%8D%E5%B8%B8%E6%9B%B4%E6%96%B0%E7%9A%84%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84.cpp)。[测试](https://godbolt.org/z/ezh1Pdo5E)链接。标准输出可能交错，但无数据竞争。

`std::shared_timed_mutex` 具有 `std::shared_mutex` 的所有功能，并且额外支持超时功能。所以以上代码可以随意更换这两个互斥量。

## `std::recursive_mutex`

线程对已经上锁的 `std::mutex` 再次上锁是错误的，这是[*未定义行为*](https://zh.cppreference.com/w/cpp/language/ub)。然而在某些情况下，一个线程会尝试在释放一个互斥量前多次获取，所以提供了`std::recursive_mutex`。

`std::recursive_mutex` 是 C++ 标准库提供的一种互斥量类型，它允许同一线程多次锁定同一个互斥量，而不会造成死锁。当同一线程多次对同一个 `std::recursive_mutex` 进行锁定时，**只有在解锁与锁定次数相匹配时，互斥量才会真正释放**。但它并不影响不同线程对同一个互斥量进行锁定的情况。不同线程对同一个互斥量进行锁定时，会按照互斥量的规则**进行阻塞**，

```cpp
#include <iostream>
#include <thread>
#include <mutex>

std::recursive_mutex mtx;

void recursive_function(int count) {
    // 递归函数，每次递归都会锁定互斥量
    mtx.lock();
    std::cout << "Locked by thread: " << std::this_thread::get_id() << ", count: " << count << std::endl;
    if (count > 0) {
        recursive_function(count - 1); // 递归调用
    }
    mtx.unlock(); // 解锁互斥量
}

int main() {
    std::thread t1(recursive_function, 3);
    std::thread t2(recursive_function, 2);

    t1.join();
    t2.join();
}
```

> [运行](https://godbolt.org/z/aefrYbGd7)测试。

* [**`lock`**](https://zh.cppreference.com/w/cpp/thread/recursive_mutex/lock)：线程可以在递归互斥体上重复调用 `lock`。在线程调用 `unlock` 匹配次数后，所有权才会得到**释放**。
* [**`unlock`**](https://zh.cppreference.com/w/cpp/thread/recursive_mutex/unlock)：若所有权层数为 1（此线程对 [lock()](https://zh.cppreference.com/w/cpp/thread/recursive_mutex/lock) 的调用恰好比 `unlock()` 多一次 ）则**解锁互斥量**，否则将所有权层数减少 1。

我们重点的强调了一下这两个成员函数的这个概念，其实也很简单，总而言之就是 `unlock` 必须和 `lock` 的调用次数一样，才会真正解锁互斥量。

同样的，我们也可以使用 `std::lock_guard`、`std::unique_lock` 帮我们管理 `std::recursive_mutex`，而非显式调用 `lock` 与 `unlock`：

```cpp
void recursive_function(int count) {
    std::lock_guard<std::recursive_mutex> lc{ mtx };
    std::cout << "Locked by thread: " << std::this_thread::get_id() << ", count: " << count << std::endl;
    if (count > 0) {
        recursive_function(count - 1);
    }
}
```

> [运行](https://godbolt.org/z/d63zrG1cK)测试。

## `new`、`delete` 是线程安全的吗？

如果你的标准达到 **C++11**，要求下列**函数**是线程安全的：

* [`new` 运算符](https://zh.cppreference.com/w/cpp/memory/new/operator_new)和 [`delete` 运算符](https://zh.cppreference.com/w/cpp/memory/new/operator_delete)的**库**版本
* 全局 `new` 运算符和 `delete` 运算符的用户替换版本
* [std::calloc](https://zh.cppreference.com/w/cpp/memory/c/calloc)、[std::malloc](https://zh.cppreference.com/w/cpp/memory/c/malloc)、[std::realloc](https://zh.cppreference.com/w/cpp/memory/c/realloc)、[std::aligned\_alloc](https://zh.cppreference.com/w/cpp/memory/c/aligned_alloc) (C++17 起)、[std::free](https://zh.cppreference.com/w/cpp/memory/c/free)

所以以下函数在多线程运行是线程安全的：

```cpp
void f(){
    T* p = new T{};
    delete p;
}
```

内存分配、释放操作是线程安全，构造和析构不涉及共享资源。而局部对象 `p` 对于每个线程来说是独立的。换句话说，每个线程都有其自己的 `p` 对象实例，因此它们不会共享同一个对象，自然没有数据竞争。

如果 `p` 是全局对象（或者外部的，只要可被多个线程读写），多个线程同时对其进行访问和修改时，就可能会导致数据竞争和未定义行为。因此，确保全局对象的线程安全访问通常需要额外的同步措施，比如互斥量或原子操作。

```cpp
T* p = nullptr;
void f(){
    p = new T{}; // 存在数据竞争
    delete p;
}
```

即使 `p` 是局部对象，如果构造函数（析构同理）涉及读写共享资源，那么一样存在数据竞争，需要进行额外的同步措施进行保护。

```cpp
int n = 1;

struct X{
    X(int v){
        ::n += v;
    }
};

void f(){
    X* p = new X{ 1 }; // 存在数据竞争
    delete p;
}
```

> 一个直观的展示是，我们可以在构造函数中使用 `std::cout`，看到无序的输出，[例子](https://godbolt.org/z/acT7bcz8E)。

***

值得注意的是，如果是自己重载 `operator new`、`operator delete` 替换了库的**全局**版本，那么它的线程安全就要我们来保证。

```cpp
// 全局的 new 运算符，替换了库的版本
void* operator new  (std::size_t count){
    return ::operator new(count); 
}
```

以上代码是线程安全的，因为 C++11 保证了 new 运算符的库版本，即 `::operator new` 是线程安全的，我们直接调用它自然不成问题。如果你需要更多的操作，就得使用互斥量之类的方式保护了。

***

总而言之，`new` 表达式线程安全要考虑三方面：`operator new`、构造函数、修改指针。

`delete` 表达式线程安全考虑两方面：`operator delete`、析构函数。

C++ 只保证了 `operator new`、`operator delete` 这两个方面的线程安全（不包括用户定义的），其它方面就得自己保证了。前面的内容也都提到了。

## 线程存储期

**线程存储期**（也有人喜欢称作“[*线程局部存储*](https://zh.wikipedia.org/wiki/%E7%BA%BF%E7%A8%8B%E5%B1%80%E9%83%A8%E5%AD%98%E5%82%A8)”）的概念源自操作系统，是一种非常古老的机制，广泛应用于各种编程语言。线程存储期的对象在线程开始时分配，并在线程结束时释放。每个线程拥有自己独立的对象实例，互不干扰。在 C++11中，引入了[**`thread_local`**](https://zh.cppreference.com/w/cpp/keyword/thread_local)关键字，用于声明具有线程存储期的对象。不少开发者喜欢直接将声明为线程存储期的对象称为：**线程变量**；也与**全局变量**、**CPU 变量**，在一起讨论。

以下是一段示例代码，展示了 `thread_local` 关键字的使用：

```cpp
int global_counter = 0;
thread_local int thread_local_counter = 0;

void print_counters(){
    std::cout << "global：" << global_counter++ << '\n';
    std::cout << "thread_local：" << thread_local_counter++ << '\n';
}

int main(){
    std::thread{ print_counters }.join();
    std::thread{ print_counters }.join();
}
```

[**运行结果**](https://godbolt.org/z/ncxTEce7f)：

```txt
global：0
thread_local：0
global：1
thread_local：0
```

这段代码很好的展示了 `thread_local` 关键字的使用以及它的作用。每一个线程都有独立的 `thread_local_counter` 对象，它们不是同一个。

***

我知道你会有问题：“那么 C++11 之前呢？”那时开发者通常使用 POSIX 线程（Pthreads）或 Win32 线程的接口，或者依赖各家编译器的扩展。例如：

* [**POSIX**](https://pubs.opengroup.org/onlinepubs/9699919799/)：使用 `pthread_key_t` 和相关的函数（ `pthread_key_create`、`pthread_setspecific`、`pthread_getspecific` 和`pthread_key_delete`）来管理线程局部存储。
* **Win32**：使用 [TLS（Thread Local Storage）](https://learn.microsoft.com/zh-cn/windows/win32/procthread/using-thread-local-storage)机制，通过函数 `TlsAlloc`、`TlsSetValue`、`TlsGetValue` 和 `TlsFree` 来实现线程局部存储。
* **GCC**：使用 [`__thread`](https://gcc.gnu.org/onlinedocs/gcc/extensions-to-the-c-language-family/thread-local-storage.html) 。
* **MSVC**：使用 [`__declspec(thread)`](https://learn.microsoft.com/zh-cn/cpp/cpp/thread?view=msvc-170)。

POSIX 与 Win32 接口的就不再介绍了，有兴趣参见我们的链接即可。我们就拿先前的代码改成使用 GCC 与 MSVC 的编译器扩展即可。

```cpp
__thread int thread_local_counter = 0;           // GCC
__declspec(thread) int thread_local_counter = 0; // MSVC
```

MSVC 无法使用 GCC 的编译器扩展，GCC 也肯定无法使用 MSVC 的扩展，不过 Clang 编译器可以，它支持 `__thread` 与 `__declspec(thread)` 两种。Clang 默认情况就支持 GCC 的编译器扩展，如果要支持 MSVC，需要设置 [`-fms-extensions`](https://clang.llvm.org/docs/ClangCommandLineReference.html#cmdoption-clang-fms-extensions) 编译选项。

* [**`__declspec(thread)` 运行测试**](https://godbolt.org/z/eMz8Yz3hv)
* [**`__thread` 运行测试**](https://godbolt.org/z/Wx6zKhK44)

要注意的是，这些扩展并不是标准的 C++ 语言特性，它们的跨平台性和可移植性较差，我们应当使用 C++ 标准的 `thread_local`。

了解其它 API 以及编译器扩展有助于理解历史上线程存储期的演进。同时扩展知识面。

> #### 注意事项
>
> 需要注意的是，在 MSVC 的实现中，如果 `std::async` 策略为 [`launch::async`](https://zh.cppreference.com/w/cpp/thread/launch) ，但却并不是每次都创建一个新的线程，而是从线程池获取线程。**这意味着无法保证线程局部变量在任务完成时会被销毁**。如果线程被回收并用于新的 `std::async` 调用，则旧的线程局部变量仍然存在。因此，**建议不要将线程局部变量与 `std::async` 一起使用**。[文档](https://learn.microsoft.com/zh-cn/cpp/standard-library/future-functions?view=msvc-170)。
>
> 虽然还没有讲 `std::async` ，不过还是可以注意一下这个问题，我们用一个简单的示例为你展示：
>
> ```cpp
> int n = 0;
>
> struct X {
>  ~X() { ++n; }
> };
>
> thread_local X x{};
>
> void use_thread_local_x() {
>  // 如果不写此弃值表达式，那么在 Gcc 与 Clang 编译此代码，会优化掉 x
>  (void)x;
> }
>
> int main() {
>  std::cout << "使用 std::thread: \n";
>  std::thread{ use_thread_local_x }.join();
>  std::cout << n << '\n';
>
>  std::cout << "使用 std::async: \n";
>  std::async(std::launch::async, use_thread_local_x);
>  std::cout << n << '\n';
> }
> ```
>
> 在不同编译器上的输出结果：
>
> * **Linux/Windows GCC、Clang**：会输出 `1`、`2`，因为线程变量 `x` 在每个任务中被正确销毁析构。
> * **Windows MSVC**：会输出 `1`、`1`，因为线程被线程池复用，线程依然活跃，线程变量 x 也就还未释放。

## CPU 变量

CPU 变量的概念很好理解。就像线程变量为每个线程提供独立的对象实例，互不干扰一样，CPU 变量也是如此。在创建 CPU 变量时，系统上的每个 CPU 都会获得该变量的一个副本。

在 Linux 内核中，从 2.6[^1] 版本开始引入了 **Per-CPU** 变量（Per-CPU variables）功能。Per-CPU 变量是为每个处理器单独分配的变量副本，旨在减少多处理器访问共享数据时的同步开销，提升性能。每个处理器只访问自己的变量副本，不需要进行同步操作，避免了数据竞争，增强了并行处理能力。

在 Windows 内核中，没有直接对应的 Per-CPU 变量机制。

本节是偏向概念的认识，而非实际进行内核编程，C++ 语言层面也并未提供此抽象。理解 CPU 变量的概念对于系统编程和内核开发非常重要。这些概念在面试和技术讨论中常常出现，掌握这些知识不仅有助于应对面试问题，也能提升对多处理器系统性能优化的理解。

## 局部、全局、线程、CPU 变量的对比与使用

在并发编程中，不同的变量有不同的使用场景和特点。以下是局部变量、全局变量、线程变量、CPU变量的对比：

> #### 局部变量（不考虑静态局部）

* 它拥有自动存储期，随作用域开始分配，结束时释放。每个线程、每次调用都有独立实例，完全独立，几乎无需同步。

> #### 全局变量

* 拥有 ***静态（static）*** [存储期](https://zh.cppreference.com/w/cpp/language/storage_duration#.E5.AD.98.E5.82.A8.E6.9C.9F)，它的存储在**程序**开始时分配，并在程序结束时解分配；且它在主函数执行之前进行[初始化](https://zh.cppreference.com/w/cpp/language/initialization#.E9.9D.9E.E5.B1.80.E9.83.A8.E5.8F.98.E9.87.8F)。

> #### 线程变量

* 拥有 ***线程（thread）*** 存储期。它的存储在**线程**开始时分配，并在线程结束时解分配。每个线程拥有它自身的对象实例。只有声明为 thread\_local 的对象拥有此存储期（不考虑非标准用法）。它的初始化需要考虑局部与非局部两种情况：
  * 非局部变量：所有具有线程局部存储期的非局部变量的初始化，会**作为线程启动的一部分进行**，并按顺序早于线程函数的执行开始。
  * 静态局部变量：控制流首次**经过它的声明**时才会被初始化（除非它被[零初始化](https://zh.cppreference.com/w/cpp/language/zero_initialization)或[常量初始化](https://zh.cppreference.com/w/cpp/language/constant_initialization)）。在其后所有的调用中，声明都会被跳过。

> #### CPU变量

* 它在标准 C++ 中无对应抽象实现，是操作系统内核功能。它主要依赖于当前系统内核来进行使用，也无法跨平台。基本概念与线程变量类似：CPU 变量是为每个处理器单独分配的变量副本。

***

* 局部变量适合临时数据，作用域结束自动释放，几乎无需同步。
* 全局变量适合整个程序的共享状态，但需要使用同步设施进行保护。
* 线程变量适合线程的独立状态，通常无需同步。
* CPU 变量的使用是少见的，主要用于内核开发和追求极致性能的高并发场景，减少 CPU 同步开销。

总而言之，结合实际使用即可，把这四种变量拿出来进行对比，增进理解，加深印象。

## 总结

本章讨论了多线程的共享数据引发的恶性条件竞争会带来的问题。并说明了可以使用互斥量（`std::mutex`）保护共享数据，并且要注意互斥量上锁的“**粒度**”。C++标准库提供了很多工具，包括管理互斥量的管理类（`std::lock_guard`），但是互斥量只能解决它能解决的问题，并且它有自己的问题（**死锁**）。同时我们讲述了一些避免死锁的方法和技术。还讲了一下互斥量所有权转移。然后讨论了面对不同情况保护共享数据的不同方式，使用 `std::call_once()` 保护共享数据的初始化过程，使用读写锁（`std::shared_mutex`）保护不常更新的数据结构。以及特殊情况可能用到的互斥量 `recursive_mutex`，有些人可能喜欢称作：**递归锁**。然后聊了一下 `new`、`delete` 运算符的库函数实际是线程安全的。最后介绍了一下线程存储期、CPU 变量，和各种变量进行了一个对比。

下一章，我们将开始讲述同步操作，会使用到 [**Futures**](https://zh.cppreference.com/w/cpp/thread#.E6.9C.AA.E6.9D.A5.E4.BD.93)、[**条件变量**](https://zh.cppreference.com/w/cpp/thread#.E6.9D.A1.E4.BB.B6.E5.8F.98.E9.87.8F)等设施。

[^1]: [Linux 内核版本历史](https://zh.wikipedia.org/wiki/Linux%E5%86%85%E6%A0%B8%E7%89%88%E6%9C%AC%E5%8E%86%E5%8F%B2)。
