@@ -1121,13 +1121,13 @@ C++11 的 `std::this_thread::get_id()` 返回的内部类型没办法直接转
1121
1121
1122
1122
## C++20 信号量
1123
1123
1124
- C++20 引入了**信号量**,对于那些熟悉操作系统或其它并发支持库的开发者来说,这个同步设施的概念应该不会感到陌生。[信号量](https://zh.wikipedia.org/wiki/%E4%BF%A1%E5%8F%B7%E9%87%8F)源自操作系统,是一个古老而广泛应用的概念 ,在各种编程语言中都有自己的抽象实现。然而,C++ 标准库对其的支持却来得很晚,在 C++20 中才得以引入。
1124
+ C++20 引入了**信号量**,对于那些熟悉操作系统或其它并发支持库的开发者来说,这个同步设施的概念应该不会感到陌生。[信号量](https://zh.wikipedia.org/wiki/%E4%BF%A1%E5%8F%B7%E9%87%8F)源自操作系统,是一个古老而广泛应用的同步设施 ,在各种编程语言中都有自己的抽象实现。然而,C++ 标准库对其的支持却来得很晚,在 C++20 中才得以引入。
1125
1125
1126
1126
信号量是一个非常**轻量简单**的同步设施,它维护一个计数,这个计数不能小于 `0`。信号量提供两种基本操作:**释放**(增加计数)和**等待**(减少计数)。如果当前信号量的计数值为 `0`,那么执行“***等待***”操作的线程将会**一直阻塞**,直到计数大于 `0`,也就是其它线程执行了“***释放***”操作。
1127
1127
1128
1128
C++ 提供了两个信号量类型:`std::counting_semaphore` 与 `std::binary_semaphore`,定义在 [`<semaphore>`](https://zh.cppreference.com/w/cpp/header/semaphore) 中。
1129
1129
1130
- `binary_semaphore`[^4] 只是 `counting_semaphore` 的别名 :
1130
+ `binary_semaphore`[^4] 只是 `counting_semaphore` 的一个特化别名 :
1131
1131
1132
1132
```cpp
1133
1133
using binary_semaphore = counting_semaphore<1>;
@@ -1230,8 +1230,49 @@ int main() {
1230
1230
1231
1231
[ ^ 4 ] :注:** 如果信号量只有二进制的 0 或 1,称为二进制信号量(binary semaphore)** ,这就是这个类型名字的由来。
1232
1232
1233
+ ## C++20 闩与屏障
1234
+
1235
+ 闩 (latch) 与屏障 (barrier) 是线程协调机制,允许任何数量的线程阻塞** 直至期待数量的线程到达** 。闩不能重复使用,而屏障则可以。
1236
+
1237
+ - ** ` std::latch ` :单次使用的线程屏障**
1238
+ - ** ` std::barrier ` :可复用的线程屏障**
1239
+
1240
+ 它们定义在标头 ** ` <latch> ` ** 。
1241
+
1242
+ 与信号量类似,屏障也是一种古老而广泛应用的同步机制。许多系统 API 提供了对屏障机制的支持,例如 POSIX 和 Win32。此外,[ OpenMP] ( https://learn.microsoft.com/zh-cn/cpp/parallel/openmp/2-directives?view=msvc-170#263-barrier-directive ) 也提供了屏障机制来支持多线程编程。
1243
+
1244
+ ### ` std::latch `
1245
+
1246
+ “闩”,这个字其实个人觉得是不常见,“** 门闩** ” 是指们背后用来关门的棍子。好了好了,不用在意,在 C++ 中就是先前说的:* 单次使用的线程屏障* 。
1247
+
1248
+ ` latch ` 类维护着一个 [ ` std::ptrdiff_t ` ] ( https://zh.cppreference.com/w/cpp/types/ptrdiff_t ) 类型的计数[ ^ 5 ] ,且只能** 减少** 计数,** 无法增加计数** 。在创建对象的时候初始化计数器的值。线程可以阻塞,直到 latch 对象的计数减少到零。由于无法增加计数,这使得 ` latch ` 成为一种** 单次使用的屏障** 。
1249
+
1250
+ ``` cpp
1251
+ std::latch work_done{ 3 };
1252
+
1253
+ void work(){
1254
+ std::cout << "等待其它线程执行\n";
1255
+ work_done.wait(); // 等待计数为 0
1256
+ std::cout << "任务开始执行\n";
1257
+ }
1258
+ int main (){
1259
+ std::jthread thread{ work };
1260
+ std::this_thread::sleep_for (3s);
1261
+ work_done.count_down(); // 默认值是 1
1262
+ work_done.count_down(2); // 传递参数 减少计数 2
1263
+ }
1264
+ ```
1265
+
1266
+ > [ 运行] ( https://godbolt.org/z/Trhh9jdbf ) 测试。
1267
+
1268
+ 通过调用 ` wait ` 函数阻塞子线程,直到主线程调用 ` count_down ` 函数原子地将计数减至 ` 0 ` ,得以解除阻塞。相信这个例子就能很清楚的展示 ` latch ` 的使用,它的逻辑比信号量还要简单。
1269
+
1270
+ [ ^ 5 ] : 注:通常的[ 实现] ( https://github.com/microsoft/STL/blob/939513b/stl/inc/latch#L88 ) 是直接保有一个 ` std::atomic<std::ptrdiff_t> ` 私有数据成员,以保证计数修改的原子性。原子类型在我们第五章的内容会详细展开。
1271
+
1272
+ ### ` std::barrier `
1273
+
1233
1274
## 总结
1234
1275
1235
- 在并发编程中,同步操作对于并发编程至关重要。如果没有同步,线程基本上就是独立的,因其任务之间的相关性,才可作为一个整体执行(比如第二章的并行求和)。本章讨论了多种用于同步操作的工具,包括条件变量、future、promise、package_task。同时,详细介绍了 C++ 时间库的知识,以使用并发支持库中的“限时等待”。最后使用 CMake + Qt 构建了一个带有 UI 界面的示例,展示异步多线程的** 必要性** 。
1276
+ 在并发编程中,同步操作对于并发编程至关重要。如果没有同步,线程基本上就是独立的,因其任务之间的相关性,才可作为一个整体执行(比如第二章的并行求和)。本章讨论了多种用于同步操作的工具,包括条件变量、future、promise、package_task、信号量 。同时,详细介绍了 C++ 时间库的知识,以使用并发支持库中的“限时等待”。还使用 CMake + Qt 构建了一个带有 UI 界面的示例,展示异步多线程的** 必要性** 。
1236
1277
1237
1278
在讨论了 C++ 中的高级工具之后,现在让我们来看看底层工具:C++ 内存模型与原子操作。
0 commit comments