Skip to content

Commit a91adbd

Browse files
committed
完成 std::barrier 的全部内容,即完成第四章“C++20 闩与屏障”一节的全部内容 #12
1 parent 55f7dea commit a91adbd

File tree

1 file changed

+61
-0
lines changed

1 file changed

+61
-0
lines changed

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

+61
Original file line numberDiff line numberDiff line change
@@ -1359,6 +1359,67 @@ int main(){
13591359

13601360
注意输出的规律,第一轮每个数字最后一位都是 `1`,第二轮每个数字最后一位都是 `2`……以此类推,因为我们分配给每个线程的输出任务就是如此,然后利用了屏障一轮一轮地打印。
13611361

1362+
`arrive_and_wait` 等价于 `wait(arrive());`。原子地将期待**计数减少 1**,然后在当前阶段的同步点**阻塞**直至运行当前阶段的阶段完成步骤。
1363+
1364+
`arrive_and_wait()` 会在期待计数减少至 `0` 时调用我们构造 barrier 对象时传入的 lambda 表达式,并**解除**所有在阶段同步点上阻塞的线程。之后**重置期待计数为构造中指定的值**。屏障的一个阶段就完成了。
1365+
1366+
- 并发调用`barrier` 除了析构函数外的成员函数不会引起数据竞争。
1367+
1368+
另外你可能注意到我们使用了 [`std::osyncstream`](https://zh.cppreference.com/w/cpp/io/basic_osyncstream) ,它是 C++20 引入的,此处是确保输出流在多线程环境中同步,**免除数据竞争,而且将不以任何方式穿插或截断**
1369+
1370+
> 虽然 `std::cout``operator<<` 调用是线程安全的,不会被打断,但多个 `operator<<` 的调用在多线程环境中可能会**交错**,导致输出结果混乱,使用 `std::osyncstream` 就可以解决这个问题。开发者可以尝试去除 `std::osyncstream` 直接使用 `std::cout`,效果会非常明显。
1371+
1372+
---
1373+
1374+
使用 `arrive``arrive_and_wait` 减少的都是**当前屏障计数**,我们称作“*期待计数*”。不管如何减少计数,当完成一个*阶段*,就重置期待计数为**构造中指定的值**了。
1375+
1376+
标准库还提供一个函数 [`arrive_and_drop`](https://zh.cppreference.com/w/cpp/thread/barrier/arrive_and_drop) 可以改变重置的计数值:*它将所有后继阶段的初始期待计数减少一,当前阶段的期待计数也减少一*
1377+
1378+
不用感到难以理解,我们来解释一下这个概念:
1379+
1380+
```cpp
1381+
std::barrier barrier{ 4 }; // 初始化计数为 4 完成阶段重置计数也是 4
1382+
barrier.arrive_and_wait(); // 当前计数减 1,不影响之后重置计数 4
1383+
barrier.arrive_and_drop(); // 当前计数与重置之后的计数均减 1 完成阶段会重置计数为 3
1384+
```
1385+
1386+
`arrive_and_drop` 可以用来控制在需要的时候,让一些线程退出同步,如:
1387+
1388+
```cpp
1389+
std::atomic_int active_threads{ 4 };
1390+
std::barrier barrier{ 4,
1391+
[n = 1]() mutable noexcept {
1392+
std::cout << "\t第" << n++ << "轮结束,活跃线程数: " << active_threads << '\n';
1393+
}
1394+
};
1395+
1396+
void f(int thread_id){
1397+
for (int i = 0; i < 5; ++i) {
1398+
std::osyncstream{ std::cout } << "线程 " << thread_id << " 输出: " << i << '\n';
1399+
if (i == 2 && thread_id == 2) { // 假设线程ID为2的线程在输出完2后退出
1400+
std::osyncstream{ std::cout } << "线程 " << thread_id << " 完成并退出\n";
1401+
--active_threads; // 减少活跃线程数
1402+
barrier.arrive_and_drop(); // 减少当前计数 1,并减少重置计数 1
1403+
return;
1404+
}
1405+
barrier.arrive_and_wait(); // 减少计数并等待,解除阻塞时重置计数并调用函数对象
1406+
}
1407+
}
1408+
1409+
int main(){
1410+
std::vector<std::jthread> threads;
1411+
for (int i = 1; i <= 4; ++i) {
1412+
threads.emplace_back(f, i);
1413+
}
1414+
}
1415+
```
1416+
1417+
> [运行](https://godbolt.org/z/csor1bq8d)测试。
1418+
1419+
初始线程有 4 个,线程 2 在执行了两轮同步之后便直接退出了,调用 `arrive_and_drop` 函数,下一个阶段的计数会重置为 `3`,也就是只有三个活跃线程继续执行。查看输出结果,非常的直观。
1420+
1421+
这样,`arrive_and_drop` 的作用就非常明显了,使用也十分的简单。
1422+
13621423
## 总结
13631424

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

0 commit comments

Comments
 (0)