「C++ 多线程」数组求和的多种方法

文章大图来源:pixiv_id=120012990

1 传统数组求和方法

传统的数组求和方法很简单明了,直接遍历数组的每个元素然后累加答案即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
#include <vector>
#include <numeric>
#include <chrono>

long long arr_sum; // 共享数据变量,数组之和

// 数组求和函数
void get_sum(const std::vector<int>& arr){
for(size_t i = 0; i < arr.size(); i ++ ){
arr_sum += arr[i];
}
}

int main(){
// 数组数据
const int n = 1e9; // 数组大小
std::vector<int> arr(n);
std::iota(begin(arr), end(arr), 0);

// 计算起始时间点
auto start_time = std::chrono::high_resolution_clock::now();

// 数组求和
get_sum(arr);

// 计算终止时间点
auto end_time = std::chrono::high_resolution_clock::now();
std::chrono::duration<double, std::milli> dura = end_time - start_time;
std::cout << "spend time = " << dura.count() << "ms" << "\n";

// 输出数组之和
std::cout << "finally the sum = " << arr_sum << "\n";

return 0;
}
  • 数组数据

    数组的大小 n 设置的比较大,值为 1e9。我这里使用 std::vector 来创建一个可变长数组,并且使用了 <numeric> 头文件中的 std::iota 函数对数组进行赋值。

    此处数组赋值后,每个元素与其下标相同(从 0 到 n - 1 递增)。

  • 计算时间

    为了体现算法效率的高低,在代码中记录了 数组求和 起始和结束的时间点,并最后输出计算数组之和所消耗的时间。

  • 求和算法

    代码中定义了一个 get_sum 函数,用于计算数组之和。由于 n 比较大,最终答案会超过 int 的范围,所以数组之和答案需要用 long long 来记录。

    在此方法中,只是实现了一个很简单的串行算法,也就是遍历整个数组依次累加所有元素。显然,我们需要依次遍历 1e9 个元素,这个效率显然就比较低了。

可能的运行结果如下:

1
2
spend time = 2188.75ms
finally the sum = 499999999500000000

2 基于 std::mutex 互斥锁的方法

std::mutex 互斥锁 可见 「C++ 多线程」std::mutex,lock(), unlock()「C++ 多线程」std::lock_guard 基本用法

在我的电脑中,CPU 是 24 核的,我接下来都会将线程数量设置为 24

基本的实现思路其实也很简单,我们设置一个 线程数量 NUM_THREADS,由此可以得到每个线程需要执行的 数组子段的大小 step,也即 n / NUM_THREADS。当然,有可能最后一段数组可能达不到这个大小。

我们创建 NUM_THREADS 个线程,创建的第 i 个线程(i0 开始)所需要计算的数组子段的 起始位置 starti * step;计算的数组字段的 终止位置 有两种情况:如果 i == NUM_THREADS - 1,表明当前计算的是最后一个数组子段,故这个时候终止位置直接取 n 即可;否则,当前计算的是中间的一个数组子段,故此时终止位置取 i * (step + 1) 即可。

我们创建多个线程执行一个 get_sum 函数,传入数组以及起始位置、终止位置,每个线程单独计算自己所需计算的数组子段元素和。

我们用一个 共享数据变量 arr_sum 来记录整个数组的元素和,因此,我们需要通过 互斥锁 确保多个线程对共享数据变量的互斥访问和累加修改操作。

我们用一个临时变量 cur_sum 来记录当前线程计算的数组子段的和,这一部分我们并不需要使用互斥锁,因为对每个数据子段访问都是互不干扰的。我们只需要在最后对 共享数据变量 arr_sum 访问和修改的时候进行尝试获得互斥锁,保证线程互斥。这样我们就能够实现多线程 并行 地计算数组元素之和了。

之后等待所有线程执行完毕,方可得到数组元素和。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
#include <numeric>
#include <chrono>

const int NUM_THREADS = 24; // 线程数量
long long arr_sum; // 共享数据变量,数组之和
std::mutex mtx; // 互斥量

// 线程接口函数
void get_sum(const std::vector<int>& arr, int start, int end){
long long cur_sum = 0;
for(size_t i = start; i < end; i ++ ){
cur_sum += arr[i];
}
// 加锁,互斥访问和累加共享数据变量 arr_sum
std::lock_guard<std::mutex> lck(mtx);
arr_sum += cur_sum;
}

int main(){
// 数组数据输入
const int n = 1e9;
std::vector<int> arr(n);
std::iota(begin(arr), end(arr), 0);

// 计算起始时间点
auto start_time = std::chrono::high_resolution_clock::now();

// 创建多个线程
const int step = n / NUM_THREADS; // 每个线程计算的块大小
std::vector<std::thread> threads(NUM_THREADS);
for(int i = 0; i < NUM_THREADS; i ++ ){
int start = i * step; // 起始位置
int end = (i == NUM_THREADS - 1) ? n : (i + 1) * step; // 结束位置
threads[i] = std::thread(get_sum, std::ref(arr), start, end);
}

// 等待所有线程完成
for(auto& t : threads) t.join();

// 计算终止时间点
auto end_time = std::chrono::high_resolution_clock::now();
std::chrono::duration<double, std::milli> dura = end_time - start_time;
std::cout << "spend time = " << dura.count() << "ms" << "\n";

// 输出数组之和
std::cout << "finally the sum = " << arr_sum << "\n";

return 0;
}

可能的运行结果如下:

1
2
spend time = 134.825ms
finally the sum = 499999999500000000

可以看出,相比于 1 传统数组求和方法 中的串行的计算方法,效率得到了显著的提升。

3 基于 std::async 异步任务的方法

std::async 异步任务 可参考 「C++ 多线程」std::async 异步任务创建

我们同样可以使用 std::async 创建异步任务的方法来实现多线程数组求和。

使用 std::async 的好处在于其可以自动地创建线程来执行异步任务,比较简单,能够直接返回线程函数执行的结果。而且不需要额外的互斥锁的来实现多线程的互斥访问和修改。

在主线程调用 future 对象的 get() 函数等待获取预期的结果。如果有线程还没有计算完结果,那么主线程会被阻塞,直到子线程执行完毕。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <iostream>
#include <future>
#include <vector>
#include <numeric>
#include <chrono>

const int NUM_THREADS = 24; // 线程数量
long long arr_sum; // 共享数据变量,数组之和

// 异步任务执行函数,返回结果
long long get_sum(const std::vector<int>& arr, int start, int end){
long long cur_sum = 0;
for(size_t i = start; i < end; i ++ ){
cur_sum += arr[i];
}
return cur_sum;
}

int main(){
// 数组数据输入
const int n = 1e9;
std::vector<int> arr(n);
std::iota(begin(arr), end(arr), 0);

// 计算起始时间点
auto start_time = std::chrono::high_resolution_clock::now();

// 创建多个异步任务
const int step = n / NUM_THREADS; // 每个线程计算的块大小
std::vector<std::future<long long>> futures(NUM_THREADS);
for(int i = 0; i < NUM_THREADS; i ++ ){
int start = i * step;
int end = (i == NUM_THREADS - 1) ? n : (i + 1) * step;
// 创建异步任务自动创建线程执行,系统看情况选择执行策略
futures[i] = std::async(get_sum, std::ref(arr), start, end);
}

// 等待每个数组子段的结果,累加
for(auto& res : futures){
arr_sum += res.get(); // 如果没计算完会阻塞主线程
}

// 计算终止时间点
auto end_time = std::chrono::high_resolution_clock::now();
std::chrono::duration<double, std::milli> dura = end_time - start_time;
std::cout << "spend time = " << dura.count() << "ms" << "\n";

// 输出数组之和
std::cout << "finally the sum = " << arr_sum << "\n";

return 0;
}

在上面的代码中,通过 std::async 启动多个异步任务,每个任务对应计算数组的一部分元素和,std::async 会返回一个 future 对象。之后从每个 future 对象获取计算结果并累加到 全局变量 arr_sum 中,最终输出总和。

可能的运行结果如下:

1
2
spend time = 134.363ms
finally the sum = 499999999500000000

4 基于 std::promise 异步任务的方法

std::promise 异步任务 可见 「C++ 多线程」std::promise 线程通信

std::promise 实现多线程数组求和的方法和 std::async 有相似之处。

我们需要创建 NUM_THREADSstd::promise 对象创建 NUM_THREADstd::future 对象 来获取每个 std::promise 关联的 std::future 对象。

我们还需要 创建对应数量的线程 来执行异步任务,并且我们需要传递 std::promise 的引用作为参数。在线程函数中进行 set_value 来设置异步任务结果的值。最后等待每个异步任务的结果计算完毕,累加结果。

等待线程完成的操作可以放到累加每个异步任务的结果之后,因为每次调用 get() 函数等待获取结果,如果结果还没有设置(线程内还没有通过 set_value 来设置结果的值),那么主线程会被阻塞,直到 set_value 被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <iostream>
#include <thread>
#include <future>
#include <vector>
#include <numeric>
#include <chrono>

const int NUM_THREADS = 24;
long long arr_sum; // 共享数据变量,数组之和

void get_sum(const std::vector<int>& arr, int start, int end,
std::promise<long long>& pro){ // 传入 promise
long long cur_sum = 0;
for(size_t i = start; i < end; i ++ ){
cur_sum += arr[i];
}
pro.set_value(cur_sum); // 设置数组子段结果的值
}

int main(){
// 数组数据输入
const int n = 1e9;
std::vector<int> arr(n);
std::iota(begin(arr), end(arr), 0);

// 计算起始时间点
auto start_time = std::chrono::high_resolution_clock::now();

// 创建多个异步任务
const int step = n / NUM_THREADS; // 每个线程计算的块大小
std::vector<std::promise<long long>> promises(NUM_THREADS);
std::vector<std::future<long long>> futures(NUM_THREADS);
std::vector<std::thread> threads(NUM_THREADS);
for(int i = 0; i < NUM_THREADS; i ++ ){
int start = i * step;
int end = (i == NUM_THREADS - 1) ? n : (i + 1) * step;
futures[i] = promises[i].get_future(); // 获取关联的 future 对象
threads[i] = std::thread(get_sum, std::ref(arr), start, end, std::ref(promises[i]));
}

// 等待每个结果,累加结果
for(auto& res : futures){
arr_sum += res.get();
}

// 等待线程完成
for(auto& t : threads) t.join();

// 计算终止时间点
auto end_time = std::chrono::high_resolution_clock::now();
std::chrono::duration<double, std::milli> dura = end_time - start_time;
std::cout << "spend time = " << dura.count() << "ms" << "\n";

// 输出数组之和
std::cout << "finally the sum = " << arr_sum << "\n";

return 0;
}

可能的运行结果如下:

1
2
spend time = 133.086ms.
finally the sum = 499999999500000000

5 基于 std::atomic 原子操作的方法

std::atomic 原子操作 可见 「C++ 多线程」std::atomic 简单操作

我们可以采用 std::atomic 原子类型变量来存储 arr_sum。这样创建多个线程对这个原子类型变量进行访问修改时就不需要加锁了。

数组元素和 自加操作正好是原子的,所以我们当然可以采用原子操作来无锁地实现多线程数组求和。

除了 arr_sum += cur_sum; 这一处不需要加锁操作之外的其他代码,几乎和 基于 std::mutex 互斥锁的方法 的代码是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <iostream>
#include <thread>
#include <atomic>
#include <vector>
#include <numeric>
#include <chrono>

const int NUM_THREADS = 24;
std::atomic<long long> arr_sum; // 共享数据变量,数组之和

// 线程接口函数
void get_sum(const std::vector<int>& arr, int start, int end){
long long cur_sum = 0;
for(size_t i = start; i < end; i ++ ){
cur_sum += arr[i];
}
arr_sum += cur_sum; // 原子操作,无需加锁
}

int main(){
// 数组数据输入
const int n = 1e9;
std::vector<int> arr(n);
std::iota(begin(arr), end(arr), 0);

// 计算起始时间点
auto start_time = std::chrono::high_resolution_clock::now();

// 创建多个线程
const int step = n / NUM_THREADS; // 每个线程计算的块大小
std::vector<std::thread> threads(NUM_THREADS);
for(int i = 0; i < NUM_THREADS; i ++ ){
int start = i * step;
int end = (i == NUM_THREADS - 1) ? n : (i + 1) * step;
threads[i] = std::thread(get_sum, std::ref(arr), start, end);
}

// 等待所有线程完成
for(auto& t : threads) t.join();

// 计算终止时间点
auto end_time = std::chrono::high_resolution_clock::now();
std::chrono::duration<double, std::milli> dura = end_time - start_time;
std::cout << "spend time = " << dura.count() << "ms." << "\n";

// 输出数组之和
std::cout << "finally the sum = " << arr_sum << "\n";

return 0;
}

可能的运行结果如下:

1
2
spend time = 134.883ms
finally the sum = 499999999500000000

「C++ 多线程」数组求和的多种方法
https://marisamagic.github.io/2025/01/07/20250107/
作者
MarisaMagic
发布于
2025年1月7日
许可协议