Skip to content

Commit b6d0bf2

Browse files
committed
1. 修改第四章中“C++20 信号量”一节的部分措辞
2. 新建一节“C++20 闩与屏障”,完成其中 `std::latch` 的部分内容 #12
1 parent f12fa72 commit b6d0bf2

File tree

1 file changed

+44
-3
lines changed

1 file changed

+44
-3
lines changed

Diff for: md/04同步操作.md

+44-3
Original file line numberDiff line numberDiff line change
@@ -1121,13 +1121,13 @@ C++11 的 `std::this_thread::get_id()` 返回的内部类型没办法直接转
11211121
11221122
## C++20 信号量
11231123
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 中才得以引入。
11251125
11261126
信号量是一个非常**轻量简单**的同步设施,它维护一个计数,这个计数不能小于 `0`。信号量提供两种基本操作:**释放**(增加计数)和**等待**(减少计数)。如果当前信号量的计数值为 `0`,那么执行“***等待***”操作的线程将会**一直阻塞**,直到计数大于 `0`,也就是其它线程执行了“***释放***”操作。
11271127
11281128
C++ 提供了两个信号量类型:`std::counting_semaphore` 与 `std::binary_semaphore`,定义在 [`<semaphore>`](https://zh.cppreference.com/w/cpp/header/semaphore) 中。
11291129
1130-
`binary_semaphore`[^4] 只是 `counting_semaphore` 的别名
1130+
`binary_semaphore`[^4] 只是 `counting_semaphore` 的一个特化别名
11311131
11321132
```cpp
11331133
using binary_semaphore = counting_semaphore<1>;
@@ -1230,8 +1230,49 @@ int main() {
12301230

12311231
[^4]:注:**如果信号量只有二进制的 0 或 1,称为二进制信号量(binary semaphore)**,这就是这个类型名字的由来。
12321232

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+
12331274
## 总结
12341275

1235-
在并发编程中,同步操作对于并发编程至关重要。如果没有同步,线程基本上就是独立的,因其任务之间的相关性,才可作为一个整体执行(比如第二章的并行求和)。本章讨论了多种用于同步操作的工具,包括条件变量、future、promise、package_task。同时,详细介绍了 C++ 时间库的知识,以使用并发支持库中的“限时等待”。最后使用 CMake + Qt 构建了一个带有 UI 界面的示例,展示异步多线程的**必要性**
1276+
在并发编程中,同步操作对于并发编程至关重要。如果没有同步,线程基本上就是独立的,因其任务之间的相关性,才可作为一个整体执行(比如第二章的并行求和)。本章讨论了多种用于同步操作的工具,包括条件变量、future、promise、package_task、信号量。同时,详细介绍了 C++ 时间库的知识,以使用并发支持库中的“限时等待”。还使用 CMake + Qt 构建了一个带有 UI 界面的示例,展示异步多线程的**必要性**
12361277

12371278
在讨论了 C++ 中的高级工具之后,现在让我们来看看底层工具:C++ 内存模型与原子操作。

0 commit comments

Comments
 (0)