# 使用线程

在标准 C++ 中，[`std::thread`](https://zh.cppreference.com/w/cpp/thread/thread) 可以指代线程，本章中，使用线程也就是代表使用 `std::thread` （C++20 `std::jthread`）。

本章围绕着它们来讲解。

## Hello World

在我们初学 C++ 的时候应该都写过这样一段代码：

```cpp
#include <iostream>

int main(){
    std::cout << "Hello World!" << std::endl;
}
```

这段代码将"Hello World!"写入到标准输出流，换行并[刷新](https://zh.cppreference.com/w/cpp/io/manip/endl)。

我们可以启动一个线程来做这件事情：

```cpp
#include <iostream>
#include <thread>  // 引入线程支持头文件

void hello(){     // 定义一个函数用作打印任务
    std::cout << "Hello World" << std::endl;
}

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

`std::thread t{ hello };` 创建了一个线程对象 `t`，将 `hello` 作为它的[可调用(Callable)](https://zh.cppreference.com/w/cpp/named_req/Callable)对象，在新线程中执行。线程对象关联了一个线程资源，我们无需手动控制，在线程对象构造成功，就自动在新线程开始执行函数 `hello`。

`t.join();` 等待线程对象 `t` 关联的线程执行完毕，否则将一直阻塞。这里的调用是必须的，否则 `std::thread` 的析构函数将调用 [`std::terminate()`](https://zh.cppreference.com/w/cpp/error/terminate) 无法正确析构。

这是因为我们创建线程对象 `t` 的时候就关联了一个活跃的线程，调用 `join()` 就是确保线程对象关联的线程已经执行完毕，然后会修改对象的状态，让 [`std::thread::joinable()`](https://zh.cppreference.com/w/cpp/thread/thread/joinable) 返回 `false`，表示线程对象目前没有关联活跃线程。`std::thread` 的析构函数，正是通过 `joinable()` 判断线程对象目前是否有关联活跃线程，如果为 `true`，那么就当做有关联活跃线程，会调用 `std::terminate()`。

***

如你所见，std::thread 高度封装，其成员函数也很少，我们可以轻易的创建线程执行任务，不过，它的用法也还远不止如此，我们慢慢介绍。

## 当前环境支持并发线程数

使用 [`hardware_concurrency`](https://zh.cppreference.com/w/cpp/thread/thread/hardware_concurrency) 函数可以获得我们当前硬件支持的并发线程数量，它是 `std::thread` 的静态成员函数。

```cpp
#include <iostream>
#include <thread>
 
int main(){
    unsigned int n = std::thread::hardware_concurrency();
    std::cout << "支持 " << n << " 个并发线程。\n";
}
```

本节其实是要普及一下计算机常识，一些古老的书籍比如 [csapp](https://hansimov.gitbook.io/csapp/ch01-a-tour-of-computer-systems/1.9#id-1.-xian-cheng-ji-bing-fa) 应该也会提到“[**超线程技术**](https://www.intel.cn/content/www/cn/zh/gaming/resources/hyper-threading.html)”。

> [英特尔® 超线程](https://www.intel.cn/content/www/cn/zh/gaming/resources/hyper-threading.html)技术是一项硬件创新，允许在每个内核上运行多个线程。更多的线程意味着可以**并行**完成更多的工作。
>
> AMD 超线程技术被称为 SMT（Simultaneous Multi-Threading），它与英特尔的技术实现有所不同，不过使用类似。

举个例子：一款 4 核心 8 线程的 CPU，这里的 8 线程其实是指所谓的*逻辑处理器*，也意味着这颗 CPU 最多可并行执行 8 个任务。

我们的 `hardware_concurrency()` 获取的值自然也会是 **8**。

当然了，都 2024 年了，我们还得考虑一个问题：“ *英特尔从 12 代酷睿开始，为其处理器引入了全新的“**大小核**”混合设计架构*”。

比如我的 CPU `i7 13700H` 它是 14 核心，20 线程，有 8 个能效核，6 个性能核。不过我们说了，物理核心这个*通常*不看重，`hardware_concurrency()` 输出的值会为 20。

* **在进行多线程编程时，我们可以参考此值来确定创建的线程数量，以更好地利用当前硬件，从而提升程序性能。**

***

我们可以举个简单的例子运用这个值：

```cpp
template<typename ForwardIt>
auto sum(ForwardIt first, ForwardIt last){
    using value_type = std::iter_value_t<ForwardIt>;
    std::size_t num_threads = std::thread::hardware_concurrency();
    std::ptrdiff_t distance = std::distance(first, last);

    if(distance > 1024000){
        // 计算每个线程处理的元素数量
        std::size_t chunk_size = distance / num_threads;
        std::size_t remainder = distance % num_threads;

        // 存储每个线程的结果
        std::vector<value_type> results(num_threads);

        // 存储关联线程的线程对象
        std::vector<std::thread> threads;

        // 创建并启动线程
        auto start = first;
        for (std::size_t i = 0; i < num_threads; ++i) {
            auto end = std::next(start, chunk_size + (i < remainder ? 1 : 0));
            threads.emplace_back([start, end, &results, i] {
                results[i] = std::accumulate(start, end, value_type{});
            });
            start = end; // 开始迭代器不断向前
        }

        // 等待所有线程执行完毕
        for (auto& thread : threads)
            thread.join();

        // 汇总线程的计算结果
        value_type total_sum = std::accumulate(results.begin(), results.end(), value_type{});
        return total_sum;
    }

    value_type total_sum = std::accumulate(first, last, value_type{});
    return total_sum;
}
```

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

我们写了这样一个求和函数 `sum`，接受两个迭代器计算它们范围中对象的和。

我们先获取了迭代器所指向的值的类型，定义了一个别名 `value_type`，我们这里使用到的 [`std::iter_value_t`](https://zh.cppreference.com/w/cpp/iterator/iter_t) 是 C++20 引入的，[返回类型推导](https://zh.cppreference.com/w/cpp/language/function#.E8.BF.94.E5.9B.9E.E7.B1.BB.E5.9E.8B.E6.8E.A8.E5.AF.BC)是 C++14 引入。如果希望代码可以在 C++11 的环境运行也可以自行修改为：

```cpp
template<typename ForwardIt>
typename std::iterator_traits<ForwardIt>::value_type sum(ForwardIt first, ForwardIt last);
```

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

`num_threads` 是当前硬件支持的并发线程的值。[`std::distance`](https://zh.cppreference.com/w/cpp/iterator/distance) 用来计算 first 到 last 的距离，也就是我们要进行求和的元素个数了。

我们这里的设计比较简单，毕竟是初学，所以只对元素个数大于 **`1024000`** 的进行多线程求和，而小于这个值的则直接使用标准库函数 [`std::accumulate`](https://zh.cppreference.com/w/cpp/algorithm/accumulate) 求和即可。

多线程求和只需要介绍**三个**地方

1. `chunk_size` 是每个线程分配的任务，但是这是可能有余数的，比如 10 个任务分配三个线程，必然余 1。但是我们也需要执行这个任务，所以还定义了一个对象 `remainder` ，它存储的就是余数。
2. `auto end = std::next(start, chunk_size + (i < remainder ? 1 : 0));` 这行代码是获取当前线程的执行范围，其实也就是要 `chunk_size` 再加上我们的余数 `remainder` 。这里写了一个三目运算符是为了进行分配任务，比如：

   假设有 3 个线程执行，并且余数是 2。那么，每个线程的处理情况如下：

   * 当 `i = 0` 时，由于 `0 < 2`，所以这个线程会多分配一个元素。
   * 当 `i = 1` 时，同样因为 `1 < 2`，这个线程也会多分配一个元素。
   * 当 `i = 2` 时，由于 `2 >= 2`，所以这个线程只处理平均数量的元素。

   这确保了**剩余**的 2 个元素被分配给了前两个线程，而第三个线程只处理了平均数量的元素。这样就确保了所有的元素都被正确地分配给了各个线程进行处理。
3. `auto start = first;` 在创建线程执行之前先定义了一个开始迭代器。在传递给线程执行的lambda表达式中，最后一行是：`start = end;` 这是为了让迭代器一直向前。

由于求和不涉及数据竞争之类的问题，所以我们甚至可以在刚讲完 `Hello World` 就手搓了一个“**并行求和**”的简单的模板函数。主要的难度其实在于对 C++ 的熟悉程度，而非对线程类 `std::thread` 的使用了，这里反而是最简单的，无非是用容器存储线程对象管理，最后进行 `join()` 罢了。

本节代码只是为了学习，而且只是百万数据通常没必要多线程，上亿的话差不多。如果你需要多线程求和，可以使用 C++17 引入的求和算法 [`std::reduce`](https://zh.cppreference.com/w/cpp/algorithm/reduce) 并指明[执行策略](https://zh.cppreference.com/w/cpp/algorithm/execution_policy_tag)。它的效率接近我们实现的 `sum` 的两倍，当前环境核心越多数据越多，和单线程效率差距越明显。

## 线程管理

在 C++ 标准库中，没有直接管理线程的机制，只能通过对象关联线程后，通过该对象来管理线程。类 `std::thread` 的对象就是指代线程的对象，而我们本节说的“线程管理”，其实也就是指管理 `std::thread` 对象。

### 启动新线程

使用 C++ 线程库启动线程，就是构造 std::thread 对象。

> 当然了，如果是**默认构造**之类的，那么 `std::thread` 线程对象没有关联线程的，自然也不会启动线程执行任务。
>
> ```cpp
> std::thread t; //  构造不表示线程的新 std::thread 对象
> ```

我们上一节的示例是传递了一个函数给 `std::thread` 对象，函数会在新线程中执行。`std::thread` 支持的形式还有很多，只要是[可调用(Callable)](https://zh.cppreference.com/w/cpp/named_req/Callable)对象即可，比如重载了 `operator()` 的类对象（也可以直接叫函数对象）。

```cpp
class Task{
public:
    void operator()()const {
        std::cout << "operator()()const\n";
    }
};
```

我们显然没办法直接像函数使用函数名一样，使用“类名”，函数名可以隐式转换到指向它的函数指针，而类名可不会直接变成对象，我们想使用 `Task` 自然就得构造对象了

```cpp
std::thread t{ Task{} };
t.join();
```

直接创建临时对象即可，可以简化代码并避免引入不必要的局部对象。

不过有件事情需要注意，当我们使用函数对象用于构造 `std::thread` 的时候，如果你传入的是一个临时对象，且使用的**都是 “`()`”小括号初始化**，那么**编译器会将此语法解析为函数声明**。

```cpp
std::thread t( Task() ); // 函数声明
```

这被编译器解析为函数声明，是一个返回类型为 `std::thread`，函数名为 `t`，接受一个返回 `Task` 的空参的函数指针类型，也就是 `Task(*)()`。

之所以我们看着抽象是因为这里的形参是无名的，且写了个函数类型。

我们用一个简单的示例为你展示：

```cpp
void h(int(int));         //#1 声明
void h(int (*p)(int)){}   //#2 定义
```

即使我还没有为你讲述概念，我相信你也发现了，#1 和 #2 的区别无非是，#1 省略了形参的名称，还有它的形参是函数类型而不是函数指针类型，没有 **`*`**。

> 在确定每个形参的类型后，类型是 “T 的数组”或某个**函数类型 T 的形参会调整为具有类型“指向 T 的指针”**。[文档](https://zh.cppreference.com/w/cpp/language/function#.E5.BD.A2.E5.8F.82.E7.B1.BB.E5.9E.8B.E5.88.97.E8.A1.A8)。

显然，`int(int)` 是一个函数类型，它被调整为了一个指向这个函数类型的指针类型。

那么回到我们最初的：

```cpp
std::thread t( Task() );                    // #1 函数声明
std::thread t( Task (*p)() ){ return {}; }  // #2 函数定义
```

`#2`我们写出了函数形参名称 `p`，再将函数类型写成函数指针类型，事实上**完全等价**。我相信，这样，也就足够了。

所以总而言之，建议使用 `{}` 进行初始化，这是好习惯，大多数时候它是合适的。

C++11 引入的 Lambda 表达式，同样可以作为构造 `std::thread` 的参数，因为 Lambda 本身就是[生成](https://cppinsights.io/s/c448ad3d)了一个函数对象，它自身就是[类类型](https://zh.cppreference.com/w/cpp/language/lambda#:~:text=lambda%20%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%98%AF%E7%BA%AF%E5%8F%B3%E5%80%BC%E8%A1%A8%E8%BE%BE%E5%BC%8F%EF%BC%8C%E5%AE%83%E7%9A%84%E7%B1%BB%E5%9E%8B%E6%98%AF%E7%8B%AC%E6%9C%89%E7%9A%84%E6%97%A0%E5%90%8D%E9%9D%9E%E8%81%94%E5%90%88%E4%BD%93%E9%9D%9E%E8%81%9A%E5%90%88%E4%BD%93%E9%9D%9E%E7%BB%93%E6%9E%84%E5%8C%96%E7%B1%BB%E7%B1%BB%E5%9E%8B%EF%BC%8C%E8%A2%AB%E7%A7%B0%E4%B8%BA%E9%97%AD%E5%8C%85%E7%B1%BB%E5%9E%8B)。

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

int main(){
    std::thread thread{ [] {std::cout << "Hello World!\n"; } };
    thread.join();
}
```

***

启动线程后（也就是构造 `std::thread` 对象）我们必须在线程对象的生存期结束之前，即 [`std::thread::~thread`](https://zh.cppreference.com/w/cpp/thread/thread/%7Ethread) 调用之前，决定它的执行策略，是 [`join()`](https://zh.cppreference.com/w/cpp/thread/thread/join)（合并）还是 [`detach()`](https://zh.cppreference.com/w/cpp/thread/thread/detach)（分离）。

我们先前使用的就是 join()，我们聊一下 **detach()**，当 `std::thread` 线程对象调用了 detach()，那么就是线程对象放弃了对线程资源的所有权，不再管理此线程，允许此线程独立的运行，在线程退出时释放所有分配的资源。

放弃了对线程资源的所有权，也就是线程对象没有关联活跃线程了，此时 joinable 为 **`false`**。

在单线程的代码中，对象销毁之后再去访问，会产生[未定义行为](https://zh.cppreference.com/w/cpp/language/ub)，多线程增加了这个问题发生的几率。

比如函数结束，那么函数局部对象的生存期都已经结束了，都被销毁了，此时线程函数还持有函数局部对象的指针或引用。

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

struct func {
    int& m_i;
    func(int& i) :m_i{ i } {}
    void operator()(int n)const {
        for (int i = 0; i <= n; ++i) {
            m_i += i;           // 可能悬空引用
        }
    }
};

int main(){
    int n = 0;
    std::thread my_thread{ func{n},100 };
    my_thread.detach();        // 分离，不等待线程结束
}                              // 分离的线程可能还在运行
```

1. 主线程（main）创建局部对象 n、创建线程对象 my\_thread 启动线程，执行任务 **`func{n}`**，局部对象 n 的引用被子线程持有。传入 100 用于调用 func 的 operator(int)。
2. `my_thread.detach();`，joinable() 为 `false`。线程分离，线程对象不再持有线程资源，线程独立的运行。
3. 主线程不等待，此时分离的子线程可能没有执行完毕，但是主线程（main）已经结束，局部对象 `n` 生存期结束，被销毁，而此时子线程还持有它的引用，访问悬空引用，造成未定义行为。`my_thread` 已经没有关联线程资源，正常析构，没有问题。

解决方法很简单，将 detach() 替换为 join()。

> **通常非常不推荐使用 detach()，因为程序员必须确保所有创建的线程正常退出，释放所有获取的资源并执行其它必要的清理操作。这意味着通过调用 detach() 放弃线程的所有权不是一种选择，因此 join 应该在所有场景中使用。** 一些老式特殊情况不聊。

另外提示一下，也**不要想着** detach() 之后，再次调用 join()

```cpp
my_thread.detach();
// todo..
my_thread.join();
// 函数结束
```

认为这样可以确保被分离的线程在这里阻塞执行完？

我们前面聊的很清楚了，detach() 是线程分离，**线程对象放弃了线程资源的所有权**，此时我们的 my\_thread 它现在根本没有关联任何线程。调用 join() 是：“阻塞当前线程直至 \*this 所标识的线程结束其执行”，我们的**线程对象都没有线程，阻塞什么？执行什么呢？**

简单点说，必须是 std::thread 的 joinable() 为 true 即线程对象有活跃线程，才能调用 join() 和 detach()。

顺带的，我们还得处理线程运行后的异常问题，举个例子：你在一个函数中构造了一个 std::thread 对象，线程开始执行，函数继续执行下面别的代码，但是如果抛出了异常呢？下面我的 **join() 就会被跳过**。

```cpp
std::thread my_thread{func{n},10};
//todo.. 抛出异常的代码
my_thread.join();
```

避免程序被抛出的异常所终止，在异常处理过程中调用 join()，从而避免线程对象析构产生问题。

```cpp
struct func; // 复用之前
void f(){
    int n = 0;
    std::thread t{ func{n},10 };
    try{
        // todo.. 一些当前线程可能抛出异常的代码
        f2();
    }
    catch (...){
        t.join(); // 1
        throw;
    }
    t.join();    // 2
}
```

我知道你可能有很多疑问，我们既然 catch 接住了异常，为什么还要 throw？以及为什么我们要两个 join()？

这两个问题其实也算一个问题，如果代码里抛出了异常，就会跳转到 catch 的代码中，执行 join() 确保线程正常执行完成，线程对象可以正常析构。然而此时我们必须再次 throw 抛出异常，因为你要是不抛出，那么你不是还得执行一个 `t.join()`？显然逻辑不对，自然抛出。

至于这个**函数产生的异常，由调用方进行处理**，我们只是确保函数 f 中创建的线程正常执行完成，其局部对象正常析构释放。[测试代码](https://godbolt.org/z/jo5sPvPGE)。

> 我知道你可能会想到：“我在 try 块中最后一行写一个 `t.join()` ，这样如果前面的代码没有抛出异常，就能正常的调用 join()，如果抛出了异常，那就调用 catch 中的 `t.join()` 根本不需要最外部 2 那里的 join()，也不需要再次 `throw` 抛出异常”
>
> ```cpp
> void f(){
>     int n = 0;
>     std::thread t{ func{n},10 };
>     try{
>         // todo.. 一些当前线程可能抛出异常的代码
>         f2();
>         t.join(); // try 最后一行调用 join()
>     }
>     catch (...){
>         t.join(); // 如果抛出异常，就在 这里调用 join()
>     }
> }
> ```
>
> 你是否觉得这样也可以？也没问题？简单的[测试](https://godbolt.org/z/Wo7Tj95Tz)运行的确没问题。
>
> **但是这是不对的**，你要注意我们的注释：“**一些当前线程可能抛出异常的代码**”，而不是 `f2()`，我们的 `try` `catch` 只是为了让线程对象关联的线程得以正确执行完毕，以及线程对象正确析构。并没有处理什么其他的东西，不掩盖错误，`try` 块中的代码抛出了异常， `catch` 接住了，我们理所应当再次抛出。

### RAII

“[资源获取即初始化](https://zh.cppreference.com/w/cpp/language/raii)”(RAII，Resource Acquisition Is Initialization)。

简单的说是：***构造函数申请资源，析构函数释放资源，让对象的生命周期和资源绑定***。当异常抛出时，C++ 会自动调用对象的析构函数。

我们可以提供一个类，在析构函数中使用 join() 确保线程执行完成，线程对象正常析构。

```cpp
class thread_guard{
    std::thread& m_t;
public:
    explicit thread_guard(std::thread& t) :m_t{ t } {}
    ~thread_guard(){
        std::puts("析构");     // 打印日志 不用在乎
        if (m_t.joinable()) { // 线程对象当前关联了活跃线程
            m_t.join();
        }
    }
    thread_guard(const thread_guard&) = delete;
    thread_guard& operator=(const thread_guard&) = delete;
};
void f(){
    int n = 0;
    std::thread t{ func{n},10 };
    thread_guard g(t);
    f2(); // 可能抛出异常
}
```

函数 f 执行完毕，局部对象就要逆序销毁了。因此，thread\_guard 对象 g 是第一个被销毁的，**调用析构函数**。**即使函数 f2() 抛出了一个异常，这个销毁依然会发生（前提是你捕获了这个异常）**。这确保了线程对象 t 所关联的线程正常的执行完毕以及线程对象的正常析构。[测试代码](https://godbolt.org/z/hn7Gced84)。

> 如果异常被抛出但未被捕获那么就会调用 [std::terminate](https://zh.cppreference.com/w/cpp/error/terminate)。是否对未捕获的异常进行任何栈回溯由**实现定义**。（简单的说就是不一定会调用析构）
>
> 我们的测试代码是捕获了异常的，为了观测，看到它一定打印“*析构*”。

在 thread\_guard 的析构函数中，我们要判断 `std::thread` 线程对象现在是否有关联的活跃线程，如果有，我们才会执行 **`join()`**，阻塞当前线程直到线程对象关联的线程执行完毕。如果不想等待线程结束可以使用 `detach()` ，但是这让 `std::thread` 对象失去了线程资源的所有权，难以掌控，具体如何，看情况分析。

复制赋值和复制构造定义为 `=delete` 可以防止编译器隐式生成，同时会[**阻止**](https://zh.cppreference.com/w/cpp/language/rule_of_three#.E4.BA.94.E4.B9.8B.E6.B3.95.E5.88.99)移动构造函数和移动赋值运算符的隐式定义。这样的话，对 thread\_guard 对象进行复制或赋值等操作会引发一个编译错误。

不允许这些操作主要在于：这是个管理类，而且顾名思义，它就应该只是单纯的管理线程对象仅此而已，只保有一个引用，**单纯的做好 RAII 的事情就行，允许其他操作没有价值。**

> 严格来说其实这里倒也不算 RAII，因为 thread\_guard 的构造函数其实并没有申请资源，只是保有了线程对象的引用，在析构的时候进行了 join() 。

### 传递参数

向可调用对象传递参数很简单，我们前面也都写了，只需要将这些参数作为 `std::thread` 的构造参数即可。需要注意的是，这些参数会复制到新线程的内存空间中，即使函数中的参数是引用，依然**实际是复制**。

```cpp
void f(int, const int& a);

int n = 1;
std::thread t{ f, 3, n };
```

线程对象 t 的构造没有问题，可以通过编译，但是这个 n 实际上并没有按引用传递，而是按值复制的。我们可以打印地址来验证我们的猜想。

```cpp
void f(int, const int& a) { // a 并非引用了局部对象 n
    std::cout << &a << '\n'; 
}

int main() {
    int n = 1;
    std::cout << &n << '\n';
    std::thread t{ f, 3, n };
    t.join();
}
```

[运行代码](https://godbolt.org/z/YPhbWbxYn)，打印的地址截然不同。

可以通过编译，但通常这不符合我们的需求，因为我们的函数中的参数是引用，我们自然希望能引用调用方传递的参数，而不是复制。如果我们的 f 的形参类型不是 **const 的引用**，则会产生一个[编译错误](https://godbolt.org/z/3nMb4asnG)。

想要解决这个问题很简单，我们可以使用标准库的设施 [`std::ref`](https://zh.cppreference.com/w/cpp/utility/functional/ref) 、 `std::cref` 函数模板。

```cpp
void f(int, int& a) {
    std::cout << &a << '\n'; 
}

int main() {
    int n = 1;
    std::cout << &n << '\n';
    std::thread t { f, 3, std::ref(n) };
    t.join();
}
```

> [运行代码](https://godbolt.org/z/zW6h1EK59)，打印地址完全相同。

我们来解释一下，“**ref**” 其实就是 “**reference**”（引用）的缩写，意思也很简单，返回“引用”，当然了，不是真的返回引用，它们返回一个包装类 [`std::reference_wrapper`](https://zh.cppreference.com/w/cpp/utility/functional/reference_wrapper)，顾名思义，这个类就是包装引用对象类模板，将对象包装，可以隐式转换为被包装对象的引用。

“**cref**”呢？，这个“c”就是“**const**”，就是返回了 `std::reference_wrapper<const T>`。我们不详细介绍他们的实现，你简单认为`reference_wrapper`可以隐式转换为被包装对象的引用即可，

```cpp
int n = 0;
std::reference_wrapper<int> r = std::ref(n);
int& p = r; // r 隐式转换为 n 的引用 此时 p 引用的就是 n
```

```cpp
int n = 0;
std::reference_wrapper<const int> r = std::cref(n);
const int& p = r; // r 隐式转换为 n 的 const 的引用 此时 p 引用的就是 n
```

> 如果对他们的实现感兴趣，可以观看[视频](https://www.bilibili.com/video/BV1np4y1j78L)。

***

以上代码`void f(int, int&)` 如果不使用 `std::ref` 并不会和前面 `void f(int, const int&)` 一样只是多了复制，而是会产生[**编译错误**](https://godbolt.org/z/xhrhs6Ke5)，这是因为 `std::thread` 内部会将保有的参数副本转换为**右值表达式进行传递**，这是为了那些**只支持移动的类型**，左值引用没办法引用右值表达式，所以产生编译错误。

```cpp
struct move_only {
    move_only() { std::puts("默认构造"); }
    move_only(const move_only&) = delete;
    move_only(move_only&&)noexcept {
        std::puts("移动构造");
    }
};

void f(move_only){}

int main(){
    move_only obj;
    std::thread t{ f,std::move(obj) };
    t.join();
}
```

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

没有 `std::ref` 自然是会保有一个副本，所以有两次移动构造，一次是被 `std::thread` 构造函数中初始化副本，一次是调用函数 `f`。

如果还有不理解，不用担心，记住，这一切的问题都会在后面的 [`std::thread` 的构造-源码解析](#stdthread-的构造-源码解析) 解释清楚。

***

[**成员函数指针**](https://zh.cppreference.com/w/cpp/language/pointer#.E6.88.90.E5.91.98.E5.87.BD.E6.95.B0.E6.8C.87.E9.92.88)也是[*可调用*](https://zh.cppreference.com/w/cpp/named_req/Callable)(*Callable*)的 ，可以传递给 `std::thread` 作为构造参数，让其关联的线程执行成员函数。

```cpp
struct X{
    void task_run(int)const;
};

 X x;
 int n = 0;
 std::thread t{ &X::task_run,&x,n };
 t.join();
```

传入成员函数指针、与其配合使用的对象、调用成员函数的参数，构造线程对象 `t`，启动线程。

如果你是第一次见到成员指针，那么我们稍微聊一下，`&X::task_run` 是一个整体，它们构成了成员指针，**&类名::非静态成员**。

成员指针必须和对象一起使用，这是唯一标准用法，成员指针不可以转换到函数指针单独使用，即使是非静态成员函数没有使用任何数据成员。

我们还可以使用模板函数 [`std::bind`](https://zh.cppreference.com/w/cpp/utility/functional/bind)与成员指针一起使用

```cpp
std::thread t{ std::bind(&X::task_run, &x ,n) };
```

不过需要注意，`std::bind` 也是默认按值[**复制**](https://godbolt.org/z/c5bh8Easd)的，即使我们的成员函数形参类型为引用：

```cpp
struct X {
    void task_run(int& a)const{
        std::cout << &a << '\n';
    }
};

X x;
int n = 0;
std::cout << &n << '\n';
std::thread t{ std::bind(&X::task_run,&x,n) };
t.join();
```

除非给参数 `n` 加上 `std::ref`，就是按[引用](https://godbolt.org/z/P9Gn5b66W)传递了：

```cpp
std::thread t{ std::bind(&X::task_run,&x,std::ref(n)) };
```

***

```cpp
void f(const std::string&);
std::thread t{ f,"hello" };
```

代码创建了一个调用 `f("hello")` 的线程。注意，函数 `f` 实际需要的是一个 `std::string` 类型的对象作为参数，但这里使用的是字符串字面量，我们要明白“***A的引用只能引用A，或者以任何形式转换到A***”，字符串字面量的类型是 `const char[N]` ，它会退化成指向它的`const char*` 指针，被线程对象保存。在调用 `f` 的时候，这个指针可以通过 `std::string` 的转换构造函数，构造出一个临时的 `std::string` 对象，就能成功调用。

字符串字面量具有静态[*存储期*](https://zh.cppreference.com/w/cpp/language/storage_duration#.E5.AD.98.E5.82.A8.E6.9C.9F)，指向它的指针这当然没问题了，不用担心生存期的问题，但是如果是指向“动态”对象的指针，就要特别注意了：

```cpp
void f(const std::string&);
void test(){
    char buffer[1024]{};
    //todo.. code
    std::thread t{ f,buffer };
    t.detach();
}
```

以上代码可能导致一些问题，buffer 是一个数组对象，作为 `std::thread` 构造参数的传递的时候会[*`decay-copy`*](https://zh.cppreference.com/w/cpp/standard_library/decay-copy) （确保实参在按值传递时会退化） **隐式转换为了指向这个数组的指针**。

我们要特别强调，`std::thread` 构造是代表“启动线程”，而不是调用我们传递的可调用对象。

`std::thread` 的构造函数中调用了创建线程的函数（windows 下可能为 [`_beginthreadex`](https://learn.microsoft.com/zh-cn/cpp/c-runtime-library/reference/beginthread-beginthreadex?view=msvc-170)），它将我们传入的参数，f、buffer ，传递给这个函数，在新线程中执行函数 `f`。也就是说，调用和执行 `f(buffer)` 并不是说要在 `std::thread` 的构造函数中，而是在创建的新线程中，具体什么时候执行，取决于操作系统的调度，所以完全有可能函数 `test` 先执行完，而新线程此时还没有进行 `f(buffer)` 的调用，转换为`std::string`，那么 buffer 指针就**悬空**了，会导致问题。解决方案：

1. 将 `detach()` 替换为 `join()`。

   ```cpp
   void f(const std::string&);
   void test(){
       char buffer[1024]{};
       //todo.. code
       std::thread t{ f,buffer };
       t.join();
   }
   ```
2. 显式将 `buffer` 转换为 `std::string`。

   ```cpp
   void f(const std::string&);
   void test(){
       char buffer[1024]{};
       //todo.. code
       std::thread t{ f,std::string(buffer) };
       t.detach();
   }
   ```

### `std::this_thread`

这个命名空间包含了管理当前线程的函数。

1. [`yield`](https://zh.cppreference.com/w/cpp/thread/yield) 建议实现重新调度各执行线程。
2. [`get_id`](https://zh.cppreference.com/w/cpp/thread/get_id) 返回当前线程 id。
3. [`sleep_for`](https://zh.cppreference.com/w/cpp/thread/sleep_for) 使当前线程停止执行指定时间。
4. [`sleep_until`](https://zh.cppreference.com/w/cpp/thread/sleep_until) 使当前线程执行**停止到**指定的时间点。

它们之中最常用的是 `get_id`，其次是 `sleep_for`，再然后 `yield`，`sleep_until` 较少。

* 使用 `get_id` [打印](https://godbolt.org/z/fPcaj7xTv)主线程和子线程的 ID。

  ```cpp
  int main() {
      std::cout << std::this_thread::get_id() << '\n';

      std::thread t{ [] {
          std::cout << std::this_thread::get_id() << '\n';
      } };
      t.join();
  }
  ```
* 使用 `sleep_for` 延时。当 `Sleep` 之类的就行，但是它需要接受的参数不同，是 `std::chrono` 命名空间中的时间对象。

  ```cpp
  int main() {
      std::this_thread::sleep_for(std::chrono::seconds(3));
  }
  ```

  主线程延时 3 秒，这个传入了一个临时对象 `seconds` ，它是模板 [`std::chrono::duration`](https://zh.cppreference.com/w/cpp/chrono/duration) 的别名，以及还有很多其他的时间类型，都基于这个类。说实话挺麻烦的，如果您支持 C++14，建议使用[时间字面量](https://zh.cppreference.com/w/cpp/symbol_index/chrono_literals)，在 [`std::chrono_literals`](https://github.com/microsoft/STL/blob/8e2d724cc1072b4052b14d8c5f81a830b8f1d8cb/stl/inc/__msvc_chrono.hpp#L718-L780) 命名空间中。我们可以改成下面这样：

  ```cpp
  using namespace std::chrono_literals;

  int main() {
      std::this_thread::sleep_for(3s);
  }
  ```

  简单直观。
* `yield` 减少 CPU 的占用。

  ```cpp
  while (!isDone()){
      std::this_thread::yield();
  }
  ```

  线程需要等待某个操作完成，如果你直接用一个循环不断判断这个操作是否完成就会使得这个线程占满 CPU 时间，这会造成资源浪费。此时可以判断操作是否完成，如果还没完成就调用 yield 交出 CPU 时间片让其他线程执行，过一会儿再来判断是否完成，这样这个线程占用 CPU 时间会大大减少。
* 使用 `sleep_until` 让当前线程延迟到具体的时间。我们延时 5 秒就是。

  ```cpp
  int main() {
      // 获取当前时间点
      auto now = std::chrono::system_clock::now();

      // 设置要等待的时间点为当前时间点之后的5秒
      auto wakeup_time = now + 5s;

      // 输出当前时间
      auto now_time = std::chrono::system_clock::to_time_t(now);
      std::cout << "Current time:\t\t" << std::put_time(std::localtime(&now_time), "%H:%M:%S") << std::endl;

      // 输出等待的时间点
      auto wakeup_time_time = std::chrono::system_clock::to_time_t(wakeup_time);
      std::cout << "Waiting until:\t\t" << std::put_time(std::localtime(&wakeup_time_time), "%H:%M:%S") << std::endl;

      // 等待到指定的时间点
      std::this_thread::sleep_until(wakeup_time);

      // 输出等待结束后的时间
      now = std::chrono::system_clock::now();
      now_time = std::chrono::system_clock::to_time_t(now);
      std::cout << "Time after waiting:\t" << std::put_time(std::localtime(&now_time), "%H:%M:%S") << std::endl;
  }
  ```

  `sleep_until` 本身设置使用很简单，是打印时间格式、设置时区麻烦。[运行结果](https://godbolt.org/z/4qYGbcvYW)。

介绍了一下 `std::this_thread` 命名空间中的四个函数的基本用法，我们后续会经常看到这些函数的使用，不用着急。

### `std::thread` 转移所有权

传入可调用对象以及参数，构造 `std::thread` 对象，启动线程，而线程对象拥有了线程的所有权，线程是一种系统资源，所以可称作“*线程资源*”。

std::thread 不可复制。两个 std::thread 对象不可表示一个线程，std::thread 对线程资源是独占所有权。而**移动**操作可以将一个 `std::thread` 对象的线程资源所有权转移给另一个 `std::thread` 对象。

```cpp
int main() {
    std::thread t{ [] {
        std::cout << std::this_thread::get_id() << '\n';
    } };
    std::cout << t.joinable() << '\n'; // 线程对象 t 当前关联了活跃线程 打印 1
    std::thread t2{ std::move(t) };    // 将 t 的线程资源的所有权移交给 t2
    std::cout << t.joinable() << '\n'; // 线程对象 t 当前没有关联活跃线程 打印 0
    //t.join(); // Error! t 没有线程资源
    t2.join();  // t2 当前持有线程资源
}
```

这段代码通过**移动构造**转移了线程对象 `t` 的线程资源所有权到 `t2`，这里虽然有两个 `std::thread` 对象，但是从始至终只有一个线程资源，让持有线程资源的 `t2` 对象最后调用 `join()` 阻塞让其线程执行完毕。`t` 与 `t2` 都能正常析构。

我们还可以使用移动赋值来转移线程资源的所有权：

```cpp
int main() {
    std::thread t;      // 默认构造，没有关联活跃线程
    std::cout << t.joinable() << '\n'; // 0
    std::thread t2{ [] {} };
    t = std::move(t2); // 转移线程资源的所有权到 t
    std::cout << t.joinable() << '\n'; // 1
    t.join();
    
    t2 = std::thread([] {});
    t2.join();
}
```

我们只需要介绍 `t2 = std::thread([] {})` ，临时对象是右值表达式，不用调用 `std::move`，这里相当于是将临时的 `std::thread` 对象所持有的线程资源转移给 `t2`，`t2` 再调用 `join()` 正常析构。

函数返回 `std::thread` 对象：

```cpp
std::thread f(){
    std::thread t{ [] {} };
    return t;
}

int main(){
    std::thread rt = f();
    rt.join();
}
```

这段代码可以[通过编译](https://godbolt.org/z/14d7b9qn9)，你是否感到奇怪？我们在函数 f 中创建了一个局部的 `std::thread` 对象，启动线程，然后返回它。

这里的 `return t` *重载决议*选择到了**移动构造**，将 `t` 线程资源的所有权转移给函数调用 `f()` 返回的临时 `std::thread` 对象中，然后这个临时对象再用来初始化 `rt` ，临时对象是右值表达式，这里一样选择到**移动构造**，将临时对象的线程资源所有权移交给 `rt`。此时 `rt` 具有线程资源的所有权，由它调用 `join()` 正常析构。

> 如果标准达到 C++17，强制的复制消除（RVO）保证这里少一次移动构造的开销（临时对象初始化 `rt` 的这次）。

**所有权也可以在函数内部传递**：

```cpp
void f(std::thread t){
    t.join();
}

int main(){
    std::thread t{ [] {} };
    f(std::move(t));
    f(std::thread{ [] {} });
}
```

`std::move` 将 t 转换为了一个右值表达式，初始化函数`f` 形参 `t`，选择到了移动构造转移线程资源的所有权，在函数中调用 `t.join()` 后正常析构。`std::thread{ [] {} }` 构造了一个临时对象，本身就是右值表达式，初始化函数`f` 形参 `t`，移动构造转移线程资源的所有权到 `t`，`t.join()` 后正常析构。

本节内容总体来说是很简单的，如果你有些地方无法理解，那只有一种可能，“对移动语义不了解”，不过这也不是问题，在后续我们详细介绍 `std::thread` 构造函数的源码即可，不用着急。

## [`std::thread` 的构造-源码解析](/modern-cpp-concurrent-programming/md/xiang-xi-fen-xi/01thread-de-gou-zao-yu-yuan-ma-jie-xi.md)

我们上一个大节讲解了线程管理，也就是 `std::thread` 的使用，其中的重中之重就是它的构造，传递参数。我们用源码实现为各位从头讲解。

了解其实现，才能更好的使用它，同时也能解释其使用与学习中的各种问题。如：

* 如何做到的默认按值复制？
* 为什么需要 `std::ref` ？
* 如何支持只能移动的对象？
* 如何做到接受任意[可调用](https://zh.cppreference.com/w/cpp/named_req/Callable)对象？
* 如何创建的线程？
* [传递参数](#传递参数)一节中的：“*`std::thread` 内部会将保有的参数副本转换为**右值表达式进行传递***”到底是如何做到的？

当你看完 [**`std::thread` 的构造-源码解析**](/modern-cpp-concurrent-programming/md/xiang-xi-fen-xi/01thread-de-gou-zao-yu-yuan-ma-jie-xi.md) 后，可以再回过头来问问自己是否能够回答这些问题。

## 实现 `joining_thread`

这个类和 `std::thread` 的区别就是析构函数会自动 `join` 。如果您好好的学习了上一节的内容，阅读了 `std::thread` 的源码，以下内容不会对您构成任何的难度。

我们存储一个 `std::thread` 作为底层数据成员，稍微注意一下构造函数和赋值运算符的实现即可。

```cpp
class joining_thread {
    std::thread t;
public:
    joining_thread()noexcept = default;
    template<typename Callable, typename... Args>
    explicit joining_thread(Callable&& func, Args&&...args) :
        t{ std::forward<Callable>(func), std::forward<Args>(args)... } {}
    explicit joining_thread(std::thread t_)noexcept : t{ std::move(t_) } {}
    joining_thread(joining_thread&& other)noexcept : t{ std::move(other.t) } {}

    joining_thread& operator=(std::thread&& other)noexcept {
        if (joinable()) { // 如果当前有活跃线程，那就先执行完
            join();
        }
        t = std::move(other);
        return *this;
    }
    ~joining_thread() {
        if (joinable()) {
            join();
        }
    }
    void swap(joining_thread& other)noexcept {
        t.swap(other.t);
    }
    std::thread::id get_id()const noexcept {
        return t.get_id();
    }
    bool joinable()const noexcept {
        return t.joinable();
    }
    void join() {
        t.join();
    }
    void detach() {
        t.detach();
    }
    std::thread& data()noexcept {
        return t;
    }
    const std::thread& data()const noexcept {
        return t;
    }
};
```

简单[使用](https://godbolt.org/z/bM7Ka7be5)一下：

```cpp
int main(){
    std::cout << std::this_thread::get_id() << '\n';
    joining_thread thread{[]{
            std::cout << std::this_thread::get_id() << '\n';
    } };
    joining_thread thread2{ std::move(thread) };
}
```

**使用容器管理线程对象，等待线程执行结束**：

```cpp
void do_work(std::size_t id){
    std::cout << id << '\n';
}

int main(){
    std::vector<std::thread>threads;
    for (std::size_t i = 0; i < 10; ++i){
        threads.emplace_back(do_work, i); // 产生线程
    }
    for(auto& thread:threads){
        thread.join();                   // 对每个线程对象调用 join()
    }
}
```

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

线程对象代表了线程，管理线程对象也就是管理线程，这个 `vector` 对象管理 10 个线程，保证他们的执行、退出。

使用我们这节实现的 `joining_thread` 则不需要最后的循环 `join()`：

```cpp
int main(){
    std::vector<joining_thread>threads;
    for (std::size_t i = 0; i < 10; ++i){
        threads.emplace_back(do_work, i);
    }
}
```

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

如果你自己编译了这些代码，相信你注意到了，打印的是乱序的，没什么规律，而且重复运行的结果还不一样，**这是正常现象**。多线程执行就是如此，无序且操作可能被打断。使用互斥量可以解决这些问题，这也就是下一章节的内容了。

## C++20 `std::jthread`

`std::jthread` 相比于 C++11 引入的 `std::thread`，只是多了两个功能：

1. **RAII 管理**：在析构时自动调用 `join()`。
2. **线程停止功能**：线程的取消/停止。

### 零开销原则

我知道你肯定有疑问，为什么 C++20 不直接为 `std::thread` 增加这两个功能，而是创造一个新的线程类型呢？

这就是 C++ 的设计哲学，***零开销原则***：*你不需要为你没有用到的（特性）付出额外的开销*。

`std::jthread` 的通常实现就是单纯的保有 `std::thread` + [`std::stop_source`](https://zh.cppreference.com/w/cpp/thread/stop_source) 这两个数据成员：

```cpp
thread _Impl;
stop_source _Ssource;
```

[MSVC STL](https://github.com/microsoft/STL/blob/23344e2/stl/inc/thread#L435-L436)、[libstdc++](https://github.com/gcc-mirror/gcc/blob/1a5e4dd/libstdc%2B%2B-v3/include/std/thread#L290-L291)、[libc++](https://github.com/llvm/llvm-project/blob/04f01a2/libcxx/include/__thread/jthread.h#L124-L125) 均是如此。

`stop_source` 通常占 8 字节，先前 `std::thread` 源码解析详细聊过其不同标准库对其保有的成员不同，简单来说也就是 64 位环境，大小为 16 或者 8。也就是 `sizeof(std::jthread)` 的值相比 `std::thread` 会多 8 ，为 `24` 或 `16`。

引入 `std::jthread` 符合*零开销原则*，它通过创建新的类型提供了更多的功能，而没有影响到原来 `std::thread` 的性能和内存占用。

### 线程停止

第一个功能很简单，不用赘述，我们直接聊这个所谓的“**线程停止**”就好。

首先要明确，C++ 的 `std::jthread` 提供的线程停止功能并不同于常见的 POSIX 函数 [`pthread_cancel`](https://pubs.opengroup.org/onlinepubs/9699919799/)。`pthread_cancel` 是一种发送取消请求的函数，但并不是强制性的线程终止方式。目标线程的可取消性状态和类型决定了取消何时生效。当取消被执行时，进行清理和终止线程。

`std::jthread` 所谓的线程停止只是一种**基于用户代码的控制机制**，而不是一种与操作系统系统有关系的线程终止。使用 `std::stop_source` 和 [`std::stop_token`](https://zh.cppreference.com/w/cpp/thread/stop_token) 提供了一种优雅地请求线程停止的方式，**但实际上停止的决定和实现都由用户代码来完成**。

```cpp
using namespace std::literals::chrono_literals;

void f(std::stop_token stop_token, int value){
    while (!stop_token.stop_requested()){ // 检查是否已经收到停止请求
        std::cout << value++ << ' ' << std::flush;
        std::this_thread::sleep_for(200ms);
    }
    std::cout << std::endl;
}

int main(){
    std::jthread thread{ f, 1 }; // 打印 1..15 大约 3 秒
    std::this_thread::sleep_for(3s);
    // jthread 的析构函数调用 request_stop() 和 join()。
}
```

> [运行](https://godbolt.org/z/dz43PT51c)测试。截止目前（`2024/5/29` clang19 未发布） `libc++` 不完全支持 `std::jthread`，建议使用 `clang` 的开发者链接 `libstdc++` 或 `MSVC STL` 进行编译。如果非要使用 `libc++`，可以添加 [`-fexperimental-library`](https://clang.llvm.org/docs/ClangCommandLineReference.html#cmdoption-clang-fexperimental-library) 编译选项，启用不稳定库功能和实验库功能。这样，我们的这段代码就可以通过编译。

`std::jthread` 提供了三个成员函数进行所谓的**线程停止**：

* `get_stop_source`：返回与 `jthread` 对象关联的 `std::stop_source`，允许从外部请求线程停止。
* `get_stop_token`：返回与 `jthread` 对象**停止状态**关联的 `std::stop_token`，允许检查是否有停止请求。
* `request_stop`：请求线程停止。

上面这段代码并未出现这三个函数的任何一个调用，不过在 `jthread` 的析构函数中，会调用 `request_stop` 请求线程停止。

```cpp
void _Try_cancel_and_join() noexcept {
    if (_Impl.joinable()) {
        _Ssource.request_stop();
        _Impl.join();
    }
}
~jthread() {
    _Try_cancel_and_join();
}
```

至于 `std::jthread thread{ f, 1 };` 函数 f 的 `std::stop_token` 的形参是谁传递的？其实就是线程对象自己调用 `get_token()` 传递的 ，源码一眼便可发现：

```cpp
template <class _Fn, class... _Args, enable_if_t<!is_same_v<remove_cvref_t<_Fn>, jthread>, int> = 0>
_NODISCARD_CTOR_JTHREAD explicit jthread(_Fn&& _Fx, _Args&&... _Ax) {
    if constexpr (is_invocable_v<decay_t<_Fn>, stop_token, decay_t<_Args>...>) {
        _Impl._Start(_STD forward<_Fn>(_Fx), _Ssource.get_token(), _STD forward<_Args>(_Ax)...);
    } else {
        _Impl._Start(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...);
    }
}
```

也就是说虽然最初的那段代码看似什么都没调用，但是实际什么都调用了。这所谓的线程停止，其实简单来说，有点像外部给线程传递信号一样。

***

**`std::stop_source`**：

* 这是一个可以发出停止请求的类型。当你调用 `stop_source` 的 `request_stop()` 方法时，它会设置内部的停止状态为“已请求停止”。
* 任何持有与这个 `stop_source` 关联的 `std::stop_token` 对象都能检查到这个停止请求。

**`std::stop_token`**：

* 这是一个可以检查停止请求的类型。线程内部可以定期检查 `stop_token` 是否收到了停止请求。
* 通过调用 `stop_token.stop_requested()`，线程可以检测到停止状态是否已被设置为“已请求停止”。

### 总结

**零开销原则**应当很好理解。我们本节的难点只在于使用到了一些 MSVC STL 的源码实现来配合理解，其主要在于“线程停止”。线程停止设施你会感觉是一种类似于外部与线程进行某种信号通信的设施，`std::stop_source` 和 `std::stop_token` 都与线程对象关联，然后来管理函数到底如何执行。

我们并没有举很多的例子，我们觉得这一个小例子所牵扯到的内容也就足够了，关键在于理解其设计与概念。

## 总结

本章节的内容围绕着：“使用线程”，也就是"**使用 `std::thread`**"展开， `std::thread` 是我们学习 C++ 并发支持库的重中之重，在最后谈起了 C++20 引入的 `std::jthread` ，它的使用与概念也非常的简单。本章的内容在市面上并不算少见，但是却是少有的**准确与完善**。即使你早已学习乃至使用 C++ 标准库进行多线程编程，我相信本章也一定可以让你收获良多。

并且如果是第一次学习本章的内容，可能会有一些难以理解的地方。建议你多思考、多记忆，并在以后反复查看和实践。

我尽量以简单通俗的方式进行讲解。学完本章后，你可能还无法在实际环境利用多线程提升程序效率，至少还需要学习到使用互斥量来保护共享数据，才能实际应用多线程编程。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://mq-bai.gitbook.io/modern-cpp-concurrent-programming/md/02-shi-yong-xian-cheng.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
