@@ -1359,6 +1359,67 @@ int main(){
1359
1359
1360
1360
注意输出的规律,第一轮每个数字最后一位都是 ` 1 ` ,第二轮每个数字最后一位都是 ` 2 ` ……以此类推,因为我们分配给每个线程的输出任务就是如此,然后利用了屏障一轮一轮地打印。
1361
1361
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
+
1362
1423
## 总结
1363
1424
1364
1425
在并发编程中,同步操作对于并发编程至关重要。如果没有同步,线程基本上就是独立的,因其任务之间的相关性,才可作为一个整体执行(比如第二章的并行求和)。本章讨论了多种用于同步操作的工具,包括条件变量、future、promise、package_task、信号量。同时,详细介绍了 C++ 时间库的知识,以使用并发支持库中的“限时等待”。还使用 CMake + Qt 构建了一个带有 UI 界面的示例,展示异步多线程的** 必要性** 。
0 commit comments