# 纲要 > 主干纲要、Hint/线索/路标 - **线程的创建与管理**:创建、启动和管理线程 - **线程间同步**:线程之间如何协作,保护共享数据,避免条件竞争; - 锁/互斥量(`std::mutex`) - 条件变量(`std::condition_variable`) - 异步任务( `std::future<T>`,`std::async()`,`std::promise<T>`,`std::packaged_task<T>`) - 原子操作(`std::atomic` ) - 信号量(c++20) - 屏障(c++20) %% # Q&A #### 已明确 #### 待明确 > 当下仍存有的疑惑 **❓<font color="#c0504d"> 有什么问题?</font>** %% <br> # C++ 标准对并发的支持 - C++11 引入了对多线程的支持,是 C++标准**首次提供原生语言层面的多线程支持**(以 Boost 线程库为蓝本)。 - 规定了**内存模型**:引入了线程存储持续性 `thread_local` ; - 引入了用于**线程管控**、**保护共享数据**、**同步线程间操作**、**底层原子操作**的新类。 - 规定了**原子操作的语义**,提供了全方位的**原子操作库**,能直接单独操作每个位、每个字节。 - C++14 引入了用于**保护共享数据的新互斥**:读写锁,`std::shared_mutex`; - C++17 增添了一系列适合新手的 **"并行算法"函数**。 > [!quote] > > C++标准库的设计目标是 "**高效实现**" 与 "**低抽象损失**" ——假定某些代码采用了标准库所提供的工具/接口,如果将其改为直接使用底层 API,不会带来性能增益,或者收效甚微。 > <br> # C++ 多线程&同步 头文件总结 > 参见[^1] [^2] [^3] 在 C++11 之前,编写**多线程程序**只能依赖于: - **平台特定 API**:POSIX 多线程 C API、 Windows 多线程 API 等 - **第三方库**:例如 Boost 库 C++11 标准引入了**多线程并发以及同步原语**的支持,从此能够**基于 C++线程库本身**进行开发。 | | 引入标准 | | 说明 | | ---------------------- | ----- | ----------------------- | --------------------------------------------------------------------- | | `<thread>` | c++11 | 线程库 | 定义了 `thread`, `jthread` 类,以及 `yield()`, `get_id()`, `sleep_for` 等函数 | | `<mutex>` | c++11 | 互斥原语 | 定义了不同的非共享互斥量,以及 `call_once()` 等。 | | `<condition_variable>` | c++11 | 条件变量 | 定义了 `condition_variable` 和 `condition_variable_any` 类 | | `<atomic>` | c++11 | 原子操作 | 定义了原子类型,`atomic<T>` 以及原子操作,用于 "无锁编程"。 | | `<future>` | c++11 | 异步计算原语 | 定义了 `future`,`promise`,`packaged_task` 和 `async()` 等,用于 "异步任务以及结果获取"。 | | `<shared_mutex>` | c++14 | 共享互斥原语 <br>(共享锁 & 独占锁) | 定义了 `shared_mutex`, `shared_lock`,`unique_lock` 等,可用作 "读写锁"。 | | | | | | | `<semaphore>` | c++20 | 信号量 | 定义了 `binary_semaphore` 以及 `counting_semaphore` 类,二元信号量以及一般信号量。 | | `<barrier>` | c++20 | 屏障原语 | 定义了 `barrier` 类 | | `<coroutine>` | c++20 | 协程 | 定义了编写协程所需的所有功能。 | | `<stop_token>` | c++20 | `std::jthread` 的停止令牌 | 定义了 `stop_token`、`stop_source`、`stop_callback` 等 | > [!NOTE] 参见 [^3] (附录 D) > ![[_attachment/02-开发笔记/01-cpp/多线程并发相关/cpp-多线程&同步.assets/IMG-cpp-多线程&同步-95A089FF5923D55E6453FFE2355218D7.png|850]] <br><br><br> # C++ 线程库 C++标准库头文件 `<thread>` 中提供的 **`std::thread` 类** 以 "**面向对象**" 的方式提供了对多线程的支持, 将**一个 `std::thread` 实例对象**与一个 "**线程函数**" 相关联,支持以下线程管理: - **发起线程**; - **识别线程**; - **等待 or 分离线程**; - **线程归属权转移**(从一个 `std:thread` 对象转换到另一个) **==无法从线程直接返回值==**,但可以运用 `future` 通过别的方式返回结果。 > [!caution] `std::thread` 类本身没有提供获取 "**线程函数返回值**" 的接口 > > 不同于 POSIX API 中的 `pthread_join(tid, (void**)&res))` 可以获取线程函数返回值。 > 在 C++ 中只能通过**异步任务机制** `std::future` 来获取线程函数返回值。 <br> ## 线程运行 新线程通过**构建 `std::thread` 实例对象而启动**。 **每个 `std::thread` 类实例对象关联一个独立线程**,当**构造一个该类实例**时,即表示**启动/运行一个新线程**。 `std::thread` 的构造函数接受一个 "**可调用对象**" 作为线程的**起始函数**(initial function), 新线程将执行该==**线程函数**==,当函数返回时则线程随之终结。 > [!info] 应用进程本身作为一个**主线程**,其**起始函数是 `main()` 函数**。 ##### 使用示例 ```cpp title:multi_thread_exam.cpp #include <iostream> #include <thread> #include <unistd.h> using namespace std; void ThreadFunc1() { sleep(2); std::cout << "Hello from ThreadFunc1\n"; } void ThreadFunc2() { std::cout << "Hello from ThreadFunc2\n"; sleep(4); } int main() { // 每个`std::thread`类对象对应一个线程, 以线程起始函数作为参数(参数类型为函数指针). // 创建两个新进程 std::thread t1(ThreadFunc1); // 创建一个线程变量t1 std::thread t2(ThreadFunc2); // 创建一个线程变量t2 // 主线程中调用线程对象的`.join()`方法, 阻塞并等待该线程结束. // 等待两个线程完成 t1.join(); // 主线程等待线程对象t完成 t2.join(); return 0; } ``` ### 参数传递 `std::thread` 类内部的参数传递机制类似于 `std::bind()`,其构造函数: - **首项**参数: - 接受任何 "**可调用对象**"(函数指针、类成员函数指针、仿函数、lambda、bind、`std::function` 包装等)作为其首项参数,即线**程的 "==入口/起始函数=="**。 - **其余**参数: - 若首项参数是 "**成员函数指针**",则第二项参数应当是 **"类的实例对象"**; - 否则,其余参数全都为**传递给 "==线程函数==" 的参数**。 ### 获取线程 ID 每个线程都具有唯一的线程 ID,**类型为 `std::thread::id`**,支持**直接比较和拷贝操作**。 (因此也可用作关联容器的键值,或用于排序) 获取线程 ID 的两种方式: - 通过 `std::thread` 对象的 `get_id()` 方法,可获取**该线程对象==关联线程**==的线程 ID。 - 通过 `<tread>` 头文件中的 `std::this_thread::get_id()` 函数,可获取 "**==当前线程==**" 的线程 ID。 线程 ID 的值也可直接写到输出流(`std::cout`),但其值的内容本身不具备语义层面的意义。 ### 等待线程完成 `join()` `std::thread` 实例的成员函数 `join()` ,表示**将阻塞 "等待"该线程运行完成**。 对于某个确定的线程,其关联的线程对象的 `join()` 方法**只能被调用一次**。 当调用过 `join()` 后,线程对象的 `joinable()` 方法将返回 `false` 。 ### 分离线程 `detach()` `std::thread` 对象的 `detach()` 方法被调用后,将**切断该==对象==与==对应线程==之间的关联**,令**线程独立地在后台运行**。 **分离出的线程将持续运行、直到最终从线程函数返回时终止**,即使 `std::thread` 对象可能早已经被销毁。 被分离出的线程也称 "**==守护线程==**"(daemon thread),其==**归属权==和==控制权==将被转移给 "C++运行时库(Runtime Library)"**,由此保证一旦该线程结束,与之关联的资源都会被正确回收。 > [!caution] > > 使用 `detach()` 分离线程后,**如果新线程的函数中持有==指针或引用==,指向 ==旧线程的局部变量==**,则当旧线程结束后,子线程 **试图访问主线程中==已销毁的对象== 将是未定义行为**。 <br><br> ## `std::thread` 对象与 "关联线程" 的生命周期 > [!caution] 在 `std::thread` 对象被销毁前,必须确保已经调用 `.join()` 或 `.detach()` 成员函数 在构建 `std::thread` 对象创建线程后,**必须==在该 `std::thread` 对象被析构前====(例如离开作用域前)调用下列成员函数之一**, 以明确如何处理其 "**关联线程**": - 1)**调用其 `.join()` 方法,表示==将阻塞 "等待"该线程结束**==; - 2)**调用其 `.detach()` 方法,表示让==该线程独立运行**==——当 `std::thread` 对象被销毁后,线程仍将继续运行,**直至线程函数返回时才结束线程**。 如果 `std::thread` 对象在将被销毁时仍未执行上述任一操作,则其 **==析构函数==将自动调用 `std::terminate()` 终止整个进程**。 > [!caution] 调用 `join()` 或 `detach()` 前,应确保通过 `.joinable()` 方法检查 "是否有与该对象关联的执行线程" ```cpp std::thread t1(Func2); std::thread t2(Func1); if (t1.joinable()) t1.join(); if (t2.joinable()) t2.detach(); ``` <br><br> ## 线程函数的参数传递方式 创建 `std::thread` 对象时,提供给 **"线程函数"** 的**所有参数** 有两种传递方式: - (1)**默认** 以 "**==按值传递==**" 的方式 "**==拷贝==**"(对左值) 或 "**==移动==**"(对**右值**) 到 **线程的内部存储空间(独立的栈)**,<br>随后,在线程上下文中的这些**参数副本**(为 **==临时对象==,右值**)将被用来**调用==线程函数==**。 - (2)当 **通过 `std::ref()` 或 `std::cref()` 包装时**,则将会进行 "**==引用传递==**" 。 <br>(`std::thread()` 传参的内部机制类似于 `std::bind()`) > [!caution] > > 若**线程函数的形参为 "==左值引用==" 类型**,则向 `std::thread` 构造函数**直接传递 "左值"** 将会报错, > 原因在于:**传递给 `std::thread` 的参数默认会被 "==拷贝==" 到线程的内部存储空间**,而**拷贝所产生的 "临时对象"是一个==右值==,无法绑定到线程函数期望的==左值引用==上**。 > > 解决方案:当线程函数期望一个 "**左值引用**" 参数时,传递给 `std::thread` 的**实参**必须 **使用 ` std::ref() ` 包装**,以明确指示进行 "**==引用传递==**" 而非 "值传递"。 > [!quote] > By default, the arguments are ==copied== into internal sotrage, where they can be accessed by the newly created thread of exectution, and **then passed to the callable object** or function **as ==rvalues== as if they were ==temporaries==**. > [!example] 示例:向 `std::thread` 传递"引用" > > ```cpp title: pass_param_to_thread_func. cpp > void ThreadFunc1 (int& value) { // 接受一个左值引用 > cout << "This is a left value reference parameter: " << value << endl; > } > > void ThreadFunc2 (int&& value) { // 接受一个右值引用 > cout << "This is a right value reference parameter: " << value << endl; > } > > int main () { > int x = 99; > // 传递左值引用 > // std::thread t1(ThreadFunc1, x); // error: 无法将临时副本(右值)绑定到左值引用 > std::thread t1(ThreadFunc1, std::ref(x)); // 需要使用 `std::ref` > // 传递右值引用 > std::thread t2(ThreadFunc2, x); // 直接传递右值引用 > > t1.join(); > t2.join(); > } > ``` > > > > [!caution] > > 对于需要"分离"的线程,**应当避免传递 "指针" 作为线程函数的参数**,以免指针的生命周期先结束,从而**导致线程内的指针成为 "悬空指针**"。 > > ```cpp > void ThreadFunc (int i, std::string const& s) { // 一个线程函数 > std::cout << "Hello from ThreadFunc1: " << i << " " << s << std::endl; > } > > void oops () { > char buffer[1024] = "Hello World Thread!"; > // 注意下面两种写法: > // 1. 直接传递 buffer 指针 > // 2. 先用 buffer 指针实例化一个 std:: string 对象, 再传递该对象 > // 两种写法的区别在于, 第一种写法可能会导致错误, 第二种写法是正确的. > // 原因在于: > // 如果直接传递了 buffer (char*指针), C++会首先"拷贝"该指针到线程的内部存储空间中, > // 然后再在线程的上下文中调用 ThreadFunc 函数. 直到调用线程函数时, char*指针才被隐式转换为 std:: string 对象. > // 同时, 由于通过 `t.detach()` 分离了线程, 因此在构建新进程后、调用 ThreadFunc 之前, 指针 buffer 的生命周期可能已经结束, > // 于是将可能导致错误————视图访问已经被销毁的内存并隐式转换为 std:: string 对象. > // 正确的做法则是方式二, 先显式地由 buffer 创建一个 std:: string 对象, 再将该对象传递给线程函数. > > // std:: thread t (ThreadFunc, 3, buffer); // 可能导致错误 > std::thread t(ThreadFunc, 3, std::string(buffer)); // 正确. 避免悬空指针. > t.detach(); // 分离线程 > } > ``` > > <br><br> ## 线程归属权转移 每个 `std::thread` 实例关联着一个执行线程,**该线程的归属权可在不同 `std::thread` 实例间转移**, 通过 **`std::thread` 对象间的 "==移动==" 操作**(移动构造/移动赋值)实现。 转移归属权之后,**原始 `std::thread` 对象会处于一个有效但"空"的状态**,即它**不再关联任何执行线程**。<br>对于这样的对象,`joinable()` 成员函数将返回 `false`。 > [!NOTE] `std::thread` 类仅支持移动语义,不支持拷贝语义 > [!caution] 线程归属权要求 > > - 对于**任一特定的执行线程**,任何时候**只有==唯一的 `std::thread` 对象==与之关联**; > - 对于**一个 `std::thread` 实例对象**,其在任何时刻**只能与一个执行线程关联**。 > > 如果尝试将**另一个线程的归属权**转移给一个 **==已关联有执行线程==的 `std::thread` 实例对象**, > 则整个进程将触发 `std::terminate()` 终止。 > ```cpp title:move_thread_ownership.cpp /** `std::thread`类只支持移动语义, 可通过移动语义实现对"线程归属权"的转移. * 移动构造函数 * 移动赋值运算符 * * 注: std::thread 不支持拷贝语义. */ int main() { std::thread t1([](){ cout << "Hello from Lambda" << endl; }); // 1) 通过`std::thread`类的"移动构造函数"进行线程归属权的转移 std::thread t2(std::move(t1)); // `std::move()`将`std::thread`实例t1变为右值; // 2) 通过`std::thread`类的"移动赋值"实现线程归属权的转移 std::thread t3; t3 = std::move(t2); t3.join(); } ``` > [!example] 需要转移线程归属权的两个常见应用场景 > > 1. 当前函数创建新线程并置于后台运行,需要将**归属权移交给当前函数的调用者**时; > 2. 将当前函数下创建的线程的**归属权传入给某个函数调用**,由后者等待该线程结束; > > 示例: > > ```cpp > void Func1() { cout << "Hello from Func1\n"; } > void Func2() { cout << "Hello from Func2\n"; } > > void AcceptThread(std::thread t) { > if (t.joinable()) t.join(); > } > > std::thread GetAThead() { > return std::thread(Func1); > } > > > int main() { > > // GetAThead()返回一个线程对象, 转移其函数内部创建的线程的归属权. > std::thread t1 = GetAThead(); > > // 向 AcceptThread 函数传递一个线程对象, 移交线程的归属权给该函数. > AcceptThread(std::move(t1)); // 通过`std::move()`将`std::thread`实例变为右值; > AcceptThread(std::thread(Func2)); // 直接向函数传递一个临时对象(右值) > } > ``` <br><br> ## 利用 RAII 范式对 `std::thread` 对象进行再封装 > [!tip] 如何保证"新线程"在 "主线程退出前" 结束? > > > 当程序中需要保证在 "新线程" **在主线程退出前结束**时,必须同时考虑出现异常的情况,即: > > - 在**主线程正常运行**时,确保会调用 `join()`; > - 当**主线程出现==异常**==时,确保也会调用 `join()`; > > 最简单的方式是使用 `try/catch` 块: > > ```cpp > try { > // do_something_in_current_thread(); > } catch (...) { > // 如果在主线程中抛出异常, 则确保子线程的 join()方法被调用. > t1.join(); > t2.join(); > throw; > } > t1.join(); > t2.join(); > ``` > > 更推荐的,更简洁、有效的实现方式是**利用==标准的 RAII 范式==**,**将主线程的任务逻辑封装为一个类,并在==该类的析构函数==中调用各个子线程的 `join()`**: > > ```cpp > /** 使用 RAII 机制来确保线程的 join()方法被调用 > * ! 设计一个类, 该类的构造函数接受线程对象的"引用", 并作为其成员变量保存. > * ! 在该类的析构函数中, 调用线程对象的 join()方法, 从而确保子线程的 join()方法被调用. > */ > class ThreadGuard { > public: > explicit ThreadGuard(std::thread& t_) : t(t_) {} > ~ThreadGuard() { > // 检查: 可 join 时, 才进行 join > if (t.joinable()) { > t.join(); > } > } > // 禁止拷贝构造与拷贝赋值 > ThreadGuard(ThreadGuard const&) = delete; > ThreadGuard& operator=(ThreadGuard const&) = delete; > > private: > std::thread& t; // 子线程对象 > }; > > void DoSomething1(int &i) { cout << "Hello from DoSomething1: " << i << endl; } > void DoSomething2(int &i) { cout << "Hello from DoSomething2: " << i << endl; } > > int main() { > int some_local_value = 88; > > std::thread t1(DoSomething1, std::ref(some_local_value)); > std::thread t2(DoSomething2, std::ref(some_local_value)); > // 利用 RAII 范式, 构建 ThreadGuard 类对象. > // 每个线程对象对应一个 ThreadGurad 类实例对象, 确保线程的 join()方法在一定被调用. > ThreadGuard g(t1); > ThreadGuard g2(t2); > } > ``` > > > <br><br><br> # C++ 互斥锁 > 由头文件 `<mutex>` 提供 ## 总结 C++标准库**头文件 `<mutex>`** 中为 "互斥原语" 提供了下列 API: - (1)**互斥锁类**: | 类 | 说明 | | ----------------------------------------------- | ------------------ | | `std::mutex` | 最简单的互斥锁,提供基本的互斥功能; | | `std::recursive_mutex` | 可递归互斥锁 | | `std::timed_mutex`、`std::recursive_timed_mutex` | 定时锁 | - (2)**`std::lock()` 函数**——用于**同时安全地锁定两个及以上的互斥锁而不引起死锁**。 - (3)三个**类模版**,作为 "**==RAII 风格==的锁封装**"—— 与互斥锁配合使用,提供 "**基于对象生命周期**" 的锁管理: | 类模版 | 说明 | | ------------------------------ | ---------------------------------------------------- | | `std::lock_guard<T>` | **==构造对象==时自动加锁,==析构==时自动解锁**;一旦锁定,不能手动解锁 | | `std::unique_lock<T>` | 进一步支持 **"手动解锁" 和 "重新锁定"**,且提供了**延迟锁定、时间锁定、所有权转移**等功能 | | `std::scoped_lock<...>`(c++17) | 构造时**加锁==多个==互斥量**(内部基于 "死锁避免算法"),析构时**释放所有锁**; | - (4) **`std::once_flag` 类 & `std::call_once()` 函数**——用于多线程下需要 "**延迟初始化**" 的场景,**确保初始化或其他操作只执行一次**。 > [!caution] **不得向==锁所在的作用域之外==传递==指针和引用==,指向受保护的共享数据** > > 包括两种情景: > > - **避免向调用者==返回指针或引用==**,指向受保护的共享数据; > - **避免在成员函数内==调用其他外部函数时传递指针或引用==**,指向受保护的共享数据。 > > 当存在 "**游离的指针或引用**"(超出锁的作用域) 指向共享数据时,安全性则被破坏。 <br><br> ## 锁类 ### `std::mutex` 基本的互斥锁 一个 `std::mutex` 对象代表一个**最基本的互斥锁**,**其在任何时候只能被一个线程锁定**。 当某个线程**调用该对象的 `lock()` 方法加锁**之后,其它线程**再尝试调用此方法加锁则该==线程会被阻塞==,直到锁被释放**。 ```cpp std::mutex mtx; // 一个全局互斥锁 void shared_function() { mtx.lock(); // 上锁 // 访问或修改共享资源 // ... mtx.unlock(); // 解锁 } ``` > [!example] 互斥锁使用示例 > > ```cpp > /** std::mutex 使用示例 > * > * print_block 函数由一个全局互斥锁进行控制. > * 在第一个线程运行print_block函数并调用`mtx.lock()`之后, > * 当第二个线程运行print_block函数并调用`mtx.lock()`时将会阻塞, > * 直到第一个线程调用了`mtx.unlock()`解锁. > */ > std::mutex mtx; // 定义互斥锁 > > void print_block(int n, char c) { > mtx.lock(); // 上锁 > for (int i = 0; i < n; ++i) { cout << c; } > cout << '\n'; > mtx.unlock(); // 解锁 > } > > int main() { > // 由于加了锁, 因此 t1 和 t2 线程的输出不会交叉, 而是分别输出 50 个'*'和 50 个'. > // 但是, "t1 和 t2 线程谁先拿到互斥锁"是不确定的, 因此谁先输出 50 个字符是不确定的. > std::thread t1(print_block, 50, '*'); > std::thread t2(print_block, 50, '); > > t1.join(); > t2.join(); > } > ``` > > <br> ### `std::recursive_mutex` 可递归互斥锁 `std::recursive_mutex` 允许 **==同一个线程==多次锁定==同一个互斥量**==,而不会导致死锁。 **每次对`lock`的调用都必须有对应的`unlock`调用**,锁才会最终释放。 ```cpp // 递归互斥锁使用示例 std::recursive_mutex rmtx; // 一个全局递归互斥锁 void RecursiveFunction(int n) { if (n > 0) { rmtx.lock(); // 上锁 std::cout << "n = " << n << std::endl; RecursiveFunction(n - 1); rmtx.unlock(); // 解锁 } } int main() { std::thread t1(RecursiveFunction, 5); t1.join(); return 0; } ``` <br> ### `std::timed_mutex` 与 `std::recursive_timed_mutex` 可定时互斥锁 支持**在指定时间内持续请求互斥锁**,如果在指定时间内未能锁定互斥量,则**操作失败并返回**: - `.try_lock_until(time_point)`:尝试在 "**给定时间点之前**" 获取锁; - `.try_lock_for()`:尝试在 "**给定时长内**" 内获取锁; - `.unlock()`:解锁 ```cpp std::timed_mutex tm_mtx; // 定义带有超时的互斥锁 void try_lock_function() { auto now = std::chrono::steady_clock::now(); if (tm_mtx.try_lock_until(now + std::chrono::seconds(1))) { std::cout << "Successfully locked" << std::endl; std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟长时间操作 tm_mtx.unlock(); } else { std::cout << "Failed to lock" << std::endl; } } int main() { std::thread t1(try_lock_function); std::thread t2(try_lock_function); t1.join(); t2.join(); return 0; } ``` <br> ## `std::lock()` 函数 `std::lock()` 函数用于**同时安全地锁定==两个或多个==互斥量(mutex)** 而不会**引起死锁**。 其内部实现采用了 **==死锁避免算法==**(通常是 `lock-ordering` 算法),来**确保多个互斥量被安全地同时锁定**, 保证在多线程环境中也不会因为锁的顺序不同而产生死锁。 `std::lock()` 为 "**==需要同时获取多个锁==**" 的场景提供了一种安全加锁方式,避免因为获取锁的顺序不一样而引发死锁。 `std::lock()` 提供的保障是:"**all or nothing**",要么**全部成功加锁**,要么**没有获取任何锁并抛出异常**。 如果在对某些互斥量成功上锁后,却在对另一个互斥视图获取锁时失败,则**已获取的锁会被全部释放**。 使用示例: ```cpp title:std_lock_exam.cpp std::mutex mutex1, mutex2; // 两个互斥锁 void thread1() { // 安全地同时为两个互斥量加锁. std::lock(mutex1, mutex2); // std::adopt_lock参数指示std::lock_guard, 传入的std::mutex实例已经拥有互斥锁, 不得再构造函数内再次加锁 std::lock_guard<std::mutex> lock1(mutex1, std::adopt_lock); std::lock_guard<std::mutex> lock2(mutex2, std::adopt_lock); // 执行线程安全的操作 std::cout << "Thread 1\n"; } ``` <br><br> ## RAII 类模版 > [!info] `std::adopt_lock` 是一个标记类型,用作 **`std::lock_guard` 或 `std::unique_lock` 构造函数的第二参数, 指示传入的互斥量已持有锁,不需再次加锁**。 ### `std::lock_guard` 封装 类模版 `std::lock_guard<T>` 针对 **互斥锁类** 做了 RAII 封装,其构造函数接收一个 "**互斥类实例**", 将在 **==构造 `std::lock_guard` 对象时自动为该互斥量加锁==**,在 **==析构时自动解锁==**,从而保证**在对象 "生命周期" 内持有锁**。 ```cpp title:std_lock_guard.cpp std::mutex mtx; // 一个全局互斥锁 void safe_print(char c) { // 用std::lock_guard类封装互斥锁, 以确保在退出作用域时解锁. std::lock_guard<std::mutex> lock(mtx); // 构造时上锁, 析构时解锁; // 访问或修改共享资源 for (int i = 0; i < 10; ++i) { std::cout << c; } std::cout << '\n'; } // std::lock_guard类实例析构时解锁 int main() { std::thread t1(safe_print, '*'); std::thread t2(safe_print, '); t1.join(); t2.join(); return 0; } ``` ### `std::unique_lock` 封装 `std::unique_lock` 提供了相较于 `std::lock_guard` 更多的控制功能,支持**手动解锁&重新上锁**、**延迟锁定、尝试锁定、持有锁的时间**等功能。 ```cpp #include <iostream> #include <thread> #include <mutex> /* `std::unique_lock<T>` 使用示例 * 支持 "手动解锁" 和 "重新锁定",且提供了延迟锁定、时间锁定、所有权转移等功能 */ std::mutex mtx; // 定义互斥量 void flexible_print(char c) { std::unique_lock<std::mutex> lock(mtx); // 自动上锁 for (int i = 0; i < 10; ++i) { std::cout << c; } std::cout << '\n'; // 可以手动解锁 lock.unlock(); // 可以在需要时重新上锁 lock.lock(); std::cout << "Re-locked and print again\n"; } void defer_lock() { std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // 指示延迟锁定——构造时不加锁 std::cout << "Data is being prepared...\n"; lock.lock(); // 手动上锁 std::cout << "Data processed\n"; }// 析构时自动解锁 int main() { std::thread t1(flexible_print, '*'); std::thread t2(flexible_print, '); std::thread t3(defer_lock); t1.join(); t2.join(); t3.join(); return 0; } ``` ### `std::scoped_lock` 封装 `std::scoped_lock` (since C++17)是针对 "**多个互斥量**" 的 RAII 封装(相当于 `std::lock_guard` 的多锁版本), 其内部机制提供了与 `std::lock()` 函数相同的功能,**保证安全地同时为多个互斥量上锁而不会形成死锁**: ```cpp title:lock_func_exam.cpp /* `std::scoped_lock<T...>` 使用示例 * 构造时加锁多个互斥量(内部基于"死锁避免算法"),析构时释放所有锁; */ std::mutex mtx1, mtx2, mtx3; void print_numbers() { // 同时锁定多个互斥量 // c++17具有隐式类模版参数推导机制, 可依据传入构造函数的参数对象自动匹配, 因此下述两语句等价. std::scoped_lock<std::mutex, std::mutex, std::mutex> lock(mtx1, mtx2, mtx3); // std::scoped_lock lock(mtx1, mtx2, mtx3) ; std::cout << "Printing numbers from multiple mutexes..." << std::endl; } // 析构时, 释放所有锁. int main() { std::thread t(print_numbers); t.join(); } ``` ### `std::once_flag` 类与 `std::call_once()` 函数 二者搭配使用,用于**确保某些操作在程序的整个生命周期中==仅执行一次==**,常用于 "**==多线程下延迟初始化、资源创建==**" 的场景。 - **`std::once_flag` 类**: 提供一个 "**标志对象**",标记某个操作**是否已被执行过**。 - **`std::call_once(flag, func)` 函数**:一个线程安全函数,**其保证与特定 `std::once_flag` 关联的函数只会被执行一次**: - **首次被调用时,==阻塞直至目标函数完成==**;其余线程在该期间**调用 `call_once`同样会阻塞**; - 首次执行完成后,**再次调用该函数时,直接==跳过==** 使用示例: ```cpp #include <mutex> #include <thread> #include <iostream> std::once_flag flag; // 标志对象 void initialize() { std::cout << "Initializing once..." << std::endl; } void thread_func(int i) { // `std::call_once()`确保与特定"std::once_flag"对象关联的函数只执行一次 std::call_once(flag ,initialize); std::cout << "Thread " << i << " is working." << std::endl; } int main() { std::thread t1(thread_func, 1); std::thread t2(thread_func, 2); std::thread t3(thread_func, 3); t1.join(); t2.join(); t3.join(); } ``` <br><br> # C++ 共享锁(读写锁) > 头文件 `<shard_mutex>` 中引入(since C++17) C++17 标准引入了对**读写锁**(也称**共享锁**)的支持: - `std::shared_mutex` 、`std::shared_timed_lock`:**共享互斥量**,用于实现读写锁功能 - `std::shared_lock<T>` 类模版:提供对 "**共享锁**" 的 **RAII 封装**,类似于 `std::lock_guard` 以及 `std::unique_lock`; 基于上述类,**读写锁的使用方式**如下,即**对 ==`std::shared_mutex` 互斥量== 套用不同的 RAII 封装** 得到: - **共享锁**(即**读锁**):对应 ==`std::shared_lock<std::shared_mutex>` 实例==; - **独占锁**(即**写锁**):对应 `std::lock_guard<std::shared_mutex>` 或 ==`std::unique_lock<std::shared_mutex>` 实例==; > [!info] 读写锁的作用:"读-读" 可并发,"读-写" 互斥" & "写-写" 互斥: > > - 某些线程持有 "**共享锁**" 时,其他线程无法获取 "**独占锁**",必须**等待所有共享锁被释放**。 > - 任一线程持有 **独占锁** 时,其他线程都无法再获取共享锁或独占锁,**直至独占锁被释放**。 > > [!info] C++17 提供的读写锁通常实现为 "**读优先锁**",且未提供选项指定 "写优先"。 > > C++标准并未明确规定,但大多数编译器(如 GCC 和 MSVC)实现为 "**读优先**" 锁。 #### 使用示例 ```cpp #include <mutex> #include <shared_mutex> #include <thread> #include <vector> #include <chrono> #include <cstdio> using namespace std; /* C++17, 读写锁使用示例 * 默认实现通常是 "读优先锁" (标准并未明确规定, 对大对数实现如gcc和msvc是读优先的) * 基于共享互斥量 `std::shared_mutex` * - 共享锁(读锁): std::shared_lock<std::shared_mutex> 实例; * - 独占锁(写锁): std::unique_lock<std::shared_mutex> 实例; */ class SharedData { public: SharedData() : data(0) {} // 读操作 void ReadData(int id) { std::shared_lock<std::shared_mutex> rlock(mutex); // 获取共享锁(读锁) printf("Reader %d reads data: %d\n", id, data); std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟操作 } // 写操作 void WriteData(int id, int newData) { std::unique_lock<std::shared_mutex> wlock(mutex); // 获取独占锁(写锁) printf("Writer %d writes data: %d\n", id, data); data = newData; std::this_thread::sleep_for(std::chrono::milliseconds(200)); // 模拟操作 } private: int data; std::shared_mutex mutex; // 读写锁 }; int main() { SharedData shareddata; vector<std::thread> threads; // 创建多个读线程 for (int i = 0; i < 5; ++i) { threads.emplace_back(&SharedData::ReadData, &shareddata, i); } // 启动一个写线程 threads.emplace_back(&SharedData::WriteData, &shareddata, 1, 42); // 再启动几个写线程 for (int i = 5; i < 10; ++i) { threads.emplace_back(&SharedData::ReadData, &shareddata, i); } // 等待所有线程完成 for (auto& t: threads) { t.join(); } return 0; } ``` <br><br> # C++ 条件变量 > 头文件 `<conditioan_variable>` 提供 C++11 提供了**两种**条件变量的实现: - `std::condition_variable` 类 - `std::condition_variable_any` 类 两者都**必须搭配互斥锁**使用,其中 **`std::condition_variable` 仅限于与 `std::mutex` 及其 RAII 封装使用**。 `std::condition_variable` 类具有下列方法: - `.wait()`: 两种重载 - (1)`.wait(lock)`: 接收到唤醒信号时,**无条件地直接被唤醒**; - (2)`.wait(lock, predicate_function)`: 接收到唤醒信号后,**仅当==满足条件时==才被唤醒**; - 第二参数为**谓词函数**,**表示 "==等待为真的条件==",函数返回值为 `false` 时陷入阻塞等待唤醒**。 - `.notify_one();`: **唤醒一个等待线程**。 - `.notify_all();` :**唤醒所有等待线程**; > [!caution] 两个 wait 函数的 "首项参数" 要求为 `std::unique_lock`,不能是 `std::lock_guard` 或 `std::mutex` 实例本身。 > > 原因在于 **`std::lock_guard` 无法被手动解锁**, 而 **`cv.wait()` 阻塞等待时需要释放锁**。 ```cpp std::mutex; std::condition_value cv; std::unique_lock<std::mutex> lock(mtx); // 必须搭配unique_lock, 而不能是lock_guard, 因为lock_guard无法手动解锁, 而cv阻塞等待时需要释放锁. cv.wait(lock, [] { return ready; }); // 条件变量, 当ready为false时阻塞等待被唤醒. // 唤醒一个等待的线程 cv.notify_one(); // 唤醒所有等待的线程 cv.notify_all(); ``` <br><br> # C++ 异步任务 `std::future` > 由头文件 `<future>` 提供 C++11 中引入的异步操作机制 `std::future` 用于 **在异步任务与调用线程之间传递结果**,其通过 "**阻塞等待**" 来获取**异步任务**的返回值(或捕获异常),实现**线程间同步**。 ### 接口总结 (1)两种 future **类模版**: | 类模版 | 说明 | | ----------------------- | ------------------------------------------------------------------------- | | `std::future<T>` | 独占 future,同一事件仅允许关联唯一一个 `std::future` | | `std::shared_future<T>` | 共享 future,同一事件可关联多个 `std::shared_future`, <br>目标事件发生时**所有关联的实例全都会收到通知**。 | > [!caution] 模版参数 `T` 即为 "**关联数据**" 的类型,如果没有关联数据则设置为 `void`。 (2)**异步任务创建**: | 接口 | | | --------------------------------- | ------------------------------------------------------------------------ | | `std::async()` 函数 | **启动一个异步任务**(函数),返回一个 `std::future<T>` 实例,可用于**获取任务的返回值**。 | | `std::promise<T>` <br>类模版 | 用于在另一个线程中**直接==设置== "异步操作" 的结果**,从而**使得与之关联的 `std::future` 可获取该结果**。 | | `std::packaged_task<T>` <br>类模版 | 将一个 "**可调用对象**" 包装成任务(该类实例),允许异步执行, <br>并**通过 `std::future` 获取函数执行结果**。 | 从上述三种方式中均可**获得一个==关联的 `std::future<T>` 实例==**。 <br> ## `std::future<T>` 类模版 `std::future<T>` 实例对象从上述三个 "**异步任务创建**" API 中获得(以返回值形式),用于**获取==与该异步任务关联==的执行结果**,其成员函数有: - `get()` :**阻塞等待异步操作完成并获取其返回结果**,**只能==调用一次==**,而后会跳过。 - 原因是首次调用时采用 "**移动**" 操作,之后结果值不复存在。 - `wait()`:**阻塞直至异步任务完成**。 - `wait_for()`:阻塞一段指定时间; - `wait_until()`:阻塞到指定时间点 - `valid()`:判断 "**异步状态**" 是否有效。 其中,`wait_for` 以及 `wait_until` **等待函数**都返回一个**状态值**,指明是否超时或目标事件是否发生: - `std::future_status::ready`:目标事件发生; - `std::future_status::timeout`:超时; - `std::future_status::defered`:future 关联任务被延后; > [!NOTE] 关于 "异常捕获" > 当异步任务抛出异常时,其**异常会被==保存到 `future` 实例==中**,当调用其 `get()` 方法时才**再次将异常抛出**。 <br> ## `std::shared_future<T>` 类模版 `std::shared_future<T>` 基本功能及 API 同 `std::future<T>` ,差异在于: | | `std::future<T>` | `std::shared_future<T>` | | -------- | -------------------------------------- | ------------------------------ | | **结果获取** | 只能调用 `get()` 一次 <br>(会通过 "移动" 操作转移结果值) | 可以调用 `get()` 多次 | | **共享性** | 不能共享 | 支持**拷贝构造**,**所有副本实例引用同一份异步结果** | | **适用场景** | 单个线程获取异步任务结果 | **多个线程共享异步任务结果** | ##### 构造 `std::shared_future<T>` 实例 构造方式支持如下几种: 1. 通过 **`std::future` 实例的 `.share()` 方法** 创建, 2. 通过对 `std::future` 实例进行 "**移动**" 操作(`std::move()`) 构造 3. 直接以 `std::promise` 或 `std::packaged_task` 的 `.get_future()` 返回的 **`std::future` "右值" 实例进行移动构造** ```cpp // 创建 `std::shared_future<T>` 实例 // 方式一 std::future<int> fut = std::async(std::launch::async, async_task); std::shared_future<int> shared_fut = fut.share(); // 方式二 std::shared_future<int> shared_fut(std::move(fut)); // 移动后fut为空. // 方式三 std::promise<int> p; std::shared_future<int> shared_fut(p.get_future()); // 通过右值隐式转移归属权 ``` > [!tip] 不同线程间,应当通过各自的 "`std::shared_future` 副本" 来获取 "同一份" 异步结果 > > 各线程**分别持有由 "==拷贝构造==" 得到 `std::shared_future` 对象实例副本**,而**所有实例引用 "同一份" 异步结果**。 > 由此,**各线程在获取结果时,可避免数据竞争**。 > (相较于在多线程间引用单个 shared_future 实例而言,后者调用`.get()` 时需要加互斥锁) > > ![[_attachment/02-开发笔记/01-cpp/多线程并发相关/cpp-多线程&同步.assets/IMG-cpp-多线程&同步-7CC949368D3BD82F03B1DD3FA308BE4D.png|597]] > ### 使用示例 ```cpp /* `std::shared_future<T>` 使用示例. * 这一类支持通过"拷贝构造"创建多个副本, 所有副本将引用自 "同一个异步结果". */ // 模拟异步任务 int async_task() { std::this_thread::sleep_for(std::chrono::seconds(2)); return 42; } // 线程函数:模拟多线程共享异步任务结果 // 形参声明为`std::shared_future`实例的按值传递, 从而实现 "拷贝构造". void worker(std::shared_future<int> shared_fut, int id) { // 获取异步结果 printf("Thread %d result: %d\n", id, shared_fut.get()); } int main() { // 启动异步任务 std::future<int> fut = std::async(std::launch::async, async_task); // 通过`std::future`对象的`.shared()`函数获取`std::shared_future`实例. std::shared_future<int> shared_fut = fut.share(); // 创建多线程, 共享异步任务结果 std::vector<std::thread> threads; int n = 5; for (int i = 0; i < n; ++i) { threads.emplace_back(worker, shared_fut, i); } // 等待所有线程完成 for (auto& t: threads) { t.join(); } return 0; } ``` <br> ## `std::async()` 函数模版 调用 `std::async()` 后将**启动一个异步任务**(函数)并返回一个 `std::future` 对象。当**任务函数执行完成后,其返回值就由该 `std::future` 对象持有**,可通过调用后者的 `.get()` 方法获取。 ##### 运行方式 该函数首项参数可以是**枚举类`std::launch` 中的两个枚举常量**,用以指定 "**异步任务**" 运行方式: - `std::launch::deferred`:指示在**当前线程上==延后调用==任务函数**,等到在 **`future` 对象** 上调用 `.wait()` 或 `.async()` 时任务函数才**在当前线程上执行**; - `std::launch::async`:指示 **开启==专属线程==**,以运行任务函数。 ##### 使用示例: ```cpp // 首项参数可以是 `std::launch::async` 或 `std::launch::defered` // 1) `std::launch::async` 表示开启新的专属线程, 以运行任务函数; auto f1 = std::async(std::launch::async, func, param); // 2) `std::launch::defered` 表示等待调用`wait()`或`get()`时,在"当前线程"上执行. auto f2 = std::async(std::launch::deferd, func, param); // 3)省略或指定为`std::launch::async | std::lauch::defered`时, 表示取默认情况(取决于编译器实现) auto f3 = std::async(func, param); ``` <br> ## `std::packaged_task<T>` 类模版 `std::packaged_task<T>` 类模版用于对 "**可调用对象**" 进行封装,其实例本身可作为 "**可调用对象**" 被 **==直接调用== `()`**,或是**作为参数被传递,例如传递给 `std::thread`**。 其模版参数 `T` 为 "**==函数类型==**",其明确了**可传入函数(或可调用对象)的类型**。 > [!note] `std::packaged_task<T>` 主要用于将任务函数 "**抽象化**" > > 其常用于**任务调度器或线程池**,使调度器能专注于**处理 `std::packaged_task<T>` 实例**,无需考虑不同的任务函数细节(因为模版参数 T 限定了函数类型) > ```cpp int add(int a, int b) { return a + b; } int main() { // 使用`std::packaged_task<T>`包装线程函数 std::packaged_task<int(int, int)> task(add); std::future<int> fut = task.get_future(); // 获取关联future; // 创建线程执行任务 std::thread t(std::move(task), 10, 15); // 阻塞等待, 获取线程的返回值 printf("Result: %d\n", fut.get()); t.join(); return 0; } ``` <br> ## `std::promise<T>` 类实例 ```cpp /* std::promise<T> 使用示例 */ // 1) 传递正常值: 通过 `.set_value()` 方法. void thread_func(std::promise<int>& prom, int value) { int res = value * value; // 通过promise实例设置结果. prom.set_value(res + 10); } // 2) 传递"异常": 通过 `.promise()` 方法 void thread_func2(std::promise<double>& prom, double x) { // 该lambda函数可能抛出异常. auto lamb = [](double x) -> double { if (x < 0) { throw std::out_of_range("x < 0"); } return sqrt(x); }; // 将可能抛出异常的函数调用放在try-catch块中, 而在catch块中通过.set_exception传递异常. try { prom.set_value(lamb(x)); } catch(...) { prom.set_exception(std::current_exception()); } } int main() { std::promise<int> prom; // 创建promise实例 std::future<int> fut = prom.get_future(); // 获取与promise对象关联的future对象. // 启动新线程, 传递promise实例. std::thread t(thread_func, prom, 10); // 等待异步结果. printf("Result: %d\n", fut.get()); t.join(); return 0; } ``` <br><br><br> # C++ 原子操作 > 头文件 `<atomic>` 提供 C++ 对 "原子操作" 的支持:其以面向对象的方式提供了一系列 "**标准原子类型**",并以 "成员函数" 形式提供了相应的 "**原子操作 API**"。 `<atomic>` 提供了以下功能: - **原子类型**: - **标准原子类型**:**由 `std::atomic<T>` 模版对 C++内置类型特化得到**,例如 `<std::atomic<int>`; - 支持用户**自定义原子类型**(要求满足特定条件) - **原子操作 API**:每个原子类型都支持一系列原子操作 API - 加载/存储:`load()`/ `store()`; - 修改操作(加法、减法、位操作等):`fetch_add()`,`fetch_sub()`,`fetch_and()`,`fetch_or()`,`fetch_xor()` - 交换:`exchange()` - 比较并交换 CAS:`compare_exchange_weak()`,`compare_exchange_strong()` - **内存模型** - 提供了不同的 "**内存序列**"(memory ordering) 选项,支持**弱同步和强同步**。 > [!NOTE] 标准原子类型 > > ![[_attachment/02-开发笔记/01-cpp/多线程并发相关/cpp-多线程&同步.assets/IMG-cpp-多线程&同步-618EA16CE92FB2843D0C165D7434407C.png|698]] <br><br> ## 原子操作函数 ### 原子修改操作 ![[_attachment/02-开发笔记/01-cpp/多线程并发相关/cpp-多线程&同步.assets/IMG-cpp-多线程&同步-31553D86D1477448EBE8A116347EE0C4.png]] ### Swap 操作 ### CAS 操作 **比较并交换**(Compare-And-Swap,**==CAS==**)操作,其逻辑为: 1. 输入:一个**期望值变量** `expected`(以引用的形式)与一个**新值** `desired`: 2. 比较 "**目标变量当前值**" 与 "**期望值** `expected`": - 若相等,则**将 "目标变量" 更新为 "==新值==" `desired`**,返回 `true`; - 若不等,则将 "**==期望值变量==**" `expected` 更新为 "**目标变量当前值**",返回 `false`; ```cpp // C++中提供的两个CAS方法的函数原型: bool compare_exchange_weak(T& expected, T desired, memory_order success = memory_order_seq_cst, memory_order failure = memory_order_seq_cst); bool compare_exchange_strong(T& expected, T desired, memory_order success = memory_order_seq_cst, memory_order failure = memory_order_seq_cst); ``` > [!info] 两个函数的差异 > > 在无竞争的情况下: > > - `compare_exchange_weak()` 可能因为 "**==伪失败==**" 而返回 `false`; > - `compare_exchange_strong()` 保证**不会伪失败**,但性能差于前者。 > > 通常,基于 "循环重试" 的代码逻辑,**伪失败对最终结果没有影响**, > 故实现各类无锁数据结构时通常都**默认采用 `compare_exchange_weak()`**。 > > > [!info] 伪失败(spurious failure) > > > > 由于硬件级别的一些限制或优化,使得 CAS 操作受到内存或硬件逻辑的干扰, > > **导致即使 "期望值与当前值相等",但 `compare_exchange_weak()` 仍然可能失败而返回 `false`**。 > > **在伪失败的情况下**,**==`expected` 的值不会被更新==**! > [!NOTE] 在多线程环境下,两个版本的 CAS 操作都需要放在一个 `while` 循环中,**==循环重试==**。 > > 原因在于:当一个线程**在读取变量值后、即将执行 CAS 操作的间隙**,其他线程可能修改了共享变量,**导致 CAS 操作 "真失败"**。 > > 在 "**循环重试**" 的方式下,每次 CAS 操作**真失败**后,**==`expected` 的值会被更新为 "目标变量最新值"==**,重新基于目标变量的 "**最新状态**" 再次尝试应用 CAS,由此应对 "条件竞争"。 ```cpp // 使用示例: std::atomic<int> counter(10); // 原子变量 int expected = 10; // 期望值 // 若原子变量的值等于期望值, 则将原子变量的值更新为20 // 否则, 将原子变量的值赋给 "期望值"变量. if (counter.compare_exchange_strong(expected, 20)) { // 原子变量将被更新为新值. cout << "CAS Succeded, counter is now " <<counter.load() << endl; } else { // expected变量将被赋予原子变量的值. cout << "CAS failed, counter is " << counter.load() << ", expected is" << expected <<endl; } ``` #### 对原子对象的赋值 > [!caution] 两个 "原子对象" 之间**不支持复制和赋值** > > 标准原子类型不支持 "**拷贝构造**"、"**拷贝赋值**", > 拷贝构造和拷贝赋值涉及到 "**在两个独立对象上的两个独立操作**"——**先从来源读取值,再将其写出到目标对象**。 > 这一过程不可能原子化,因此,原子对象 **==禁止拷贝复制和拷贝构造==**。 > 由此,两个原子对象之间不支持 "复制" 和 "赋值"。 > 标准原子类型:支持由 "**==内建类型==**" 向对应的 "**原子类型**" 赋值(但不支持两个"原子对象" 之间的赋值) 对原子类型来说: - **赋值操作符**的返回值是 "**==存入的值==**"(非原子类型); => 有别于常规类型的赋值操作,后者是返回 "引用"; - **具名成员函数**的返回值是 "**进行操作前的旧值**"; 说明示例: ```cpp std::atomic<bool> a(true); a = false; // 支持内建类型直接向 "原子对象" 赋值. std::atomic<bool> b(false); // a = b // error: 不支持两个 "原子对象" 之间的复制. ``` <br><br> ### 内存顺序 枚举类 `std::memory_order` 中给出了 6 个可选值,用于**指定原子操作所需的 "==内存次序语义=="**。 各个原子操作函数都**可接受下列值作为 "参数"**,未指定时默认采用最严格的内存次序:`std::memory_order_seq_cst`。 ![[_attachment/02-开发笔记/01-cpp/多线程并发相关/cpp-多线程&同步.assets/IMG-cpp-多线程&同步-CCDD8D15EBD9CC67DEA03BFB370A3F45.png]] #### 操作所允许的内存次序 操作的类别**决定了内存次序所准许的取值**,操作被划分为三类,支持的内存次序如下: ![[_attachment/02-开发笔记/01-cpp/多线程并发相关/cpp-多线程&同步.assets/IMG-cpp-多线程&同步-AC14FD0B41DD53FB167D0CD862DAD902.png|497]] 其中,"**读-改-写**" 支持任意一种内存次序。 <br><br> # C++ 信号量 略。 <br><br> # Buffer ## 闪念 > sudden idea ## 候选资料 > Read it later <br> # ♾️参考资料 - 《C++并发编程实战(第 2 版)》 - 附录 D:C++11 线程库参考名录 - 附录 B:各并发程序库的简要对比 # Footnotes [^1]: [C++ Standard Library headers](https://en.cppreference.com/w/cpp/header) [^2]: 《C++ 20 高级编程》(附录 B——标准库头文件) [^3]: 《C++并发编程实战第二版》