文章大图来源: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
个线程(i
从 0
开始)所需要计算的数组子段的 起始位置 start
为 i * 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]; } 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_THREADS
个 std::promise
对象 ,创建 NUM_THREAD
个 std::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) { 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 (); 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