@@ -72,6 +72,78 @@ int main(){
72
72
73
73
- ** 在进行多线程编程的时候,我们可以参考此值确定我们要创建的线程数量**
74
74
75
+ ---
76
+
77
+ 我们可以举个简单的例子运用这个值:
78
+
79
+ ``` cpp
80
+ template <typename InputIt>
81
+ auto sum (InputIt first, InputIt last){
82
+ using value_type = std::remove_cvref_t<decltype(* first)>;
83
+ std::size_t num_threads = std::thread : :hardware_concurrency();
84
+ std::ptrdiff_t distance = std::distance(first, last);
85
+
86
+ if(distance > 1024000){
87
+ // 计算每个线程处理的元素数量
88
+ std::size_t chunk_size = distance / num_threads;
89
+ std::size_t remainder = distance % num_threads;
90
+
91
+ // 存储每个线程的结果
92
+ std::vector<value_type>results(num_threads);
93
+
94
+ // 存储关联线程的线程对象
95
+ std::vector<std::thread> threads;
96
+
97
+ // 创建并启动线程
98
+ InputIt start = first;
99
+ for (std::size_t i = 0; i < num_threads; ++i) {
100
+ InputIt end = std::next(start, chunk_size + (i < remainder ? 1 : 0));
101
+ threads.emplace_back([start, end, &results, i] {
102
+ results[i] = std::accumulate(start, end, value_type{});
103
+ });
104
+ start = end; // 开始迭代器不断向前
105
+ }
106
+
107
+ // 等待所有线程执行完毕
108
+ for (auto& thread : threads)
109
+ thread.join();
110
+
111
+ // 汇总线程的计算结果
112
+ value_type total_sum = std::accumulate(results.begin(), results.end(), value_type{});
113
+ return total_sum;
114
+ }
115
+
116
+ value_type total_sum = std::accumulate(first, last, value_type{});
117
+ return total_sum;
118
+ }
119
+ ```
120
+
121
+ > [运行](https://godbolt.org/z/YE7q1qqcK)测试。
122
+
123
+ 我们写了这样一个求和函数 `sum`,接受两个迭代器计算它们范围中对象的和。
124
+
125
+ 我们先获取了迭代器所指向的值的类型,定义了一个别名 `value_type`,记得删除 CV 与引用,我们这里使用到的 [`std::remove_cvref`](https://zh.cppreference.com/w/cpp/types/remove_cvref) 是 C++20 引入的。如果希望代码可以在 C++11 的环境运行也可以自行修改。`num_threads` 是当前硬件支持的并发线程的值。[`std::distance`](https://zh.cppreference.com/w/cpp/iterator/distance) 用来计算 first 到 last 的距离,也就是我们要进行求和的元素个数了。
126
+
127
+ 我们这里的设计比较简单,毕竟是初学,所以只对元素个数大于 **`1024000`** 的进行多线程求和,而小于这个值的则直接使用标准库函数 [`std::accumulate`](https://zh.cppreference.com/w/cpp/algorithm/accumulate) 求和即可。
128
+
129
+ 多线程求和只需要介绍**三个**地方
130
+
131
+ 1. `chunk_size` 是每个线程分配的任务,但是这是可能有余数的,比如 10 个任务分配三个线程,必然余 1。但是我们也需要执行这个任务,所以还定义了一个对象 `remainder` ,它存储的就是余数。
132
+
133
+ 2. `InputIt end = std::next(start, chunk_size + (i < remainder ? 1 : 0));` 这行代码是获取当前线程的执行范围,其实也就是要 `chunk_size` 再加上我们的余数 `remainder` 。这里写了一个三目运算符是为了进行分配任务,比如:
134
+
135
+ 假设有 3 个线程执行,并且余数是 2。那么,每个线程的处理情况如下:
136
+
137
+ - 当 `i = 0` 时,由于 `0 < 2`,所以这个线程会多分配一个元素。
138
+ - 当 `i = 1` 时,同样因为 `1 < 2`,这个线程也会多分配一个元素。
139
+ - 当 `i = 2` 时,由于 `2 >= 2`,所以这个线程只处理平均数量的元素。
140
+
141
+ 这确保了**剩余**的 2 个元素被分配给了前两个线程,而第三个线程只处理了平均数量的元素。这样就确保了所有的元素都被正确地分配给了各个线程进行处理。
142
+
143
+ 3. `InputIt start = first;` 在创建线程执行之前先定义了一个开始迭代器。在传递给线程执行的lambda表达式中,最后一行是:`start = end;` 这是为了让迭代器一直向前。
144
+
145
+ 由于求和不涉及数据竞争之类的问题,所以我们甚至可以在刚讲完 `Hello World` 就手搓了一个“**并行求和**”的简单的模板函数。主要的难度其实在于对 C++ 的熟悉程度,而非对线程类 `std::thread` 的使用了,这里反而是最简单的,无非是用容器存储线程对象管理,最后进行 `join()` 罢了。
146
+
75
147
## 线程管理
76
148
77
149
在 C++ 标准库中,只能管理与 `std::thread` 关联的线程,类 `std::thread` 的对象就是指代线程的对象,我们说“线程管理”,其实也就是管理 `std::thread` 对象。
0 commit comments