C++11学习笔记-----线程库std::thread
在以前,要想在C++程序中使用線程,需要調用操作系統提供的線程庫,比如linux下的<pthread.h>。但畢竟是底層的C函數庫,沒有什么抽象封裝可言,僅僅透露著一種簡單,暴力美
C++11在語言級別上提供了線程的支持,不考慮性能的情況下可以完全代替操作系統的線程庫,而且使用起來非常方便,為開發程序提供了很大的便利
Linux下的原生線程庫
pthread庫函數
創建線程采用pthread_create函數,函數聲明如下
#include <pthread.h> /* * pid : 線程id,傳入pthread_t類型的指針,函數返回時會返回線程id* attr : 線程屬性* func : 線程調用的函數* arg : 給函數傳入的參數*/ int pthread_create(pthread_t* pid, const pthread_attr_t* attr, void*(*func)(void*), void* arg);可以發現,創建線程時只能傳遞一個參數給線程函數,所以如果想要給函數傳入多個參數的話就需要動點歪腦筋,比如如果是類對象的話可以傳入this指針,或者也可以將參數封裝成一個struct傳進去。不過總感覺不太優雅
當一個進程創建一個線程時,雖然線程運行在主進程的內存空間中,但是每個線程也有自己的私有空間(資源,如局部變量等)。當程序正常退出時,執行者是希望線程的私有資源可以成功被操作系統回收(即資源回收),這就需要主進程在退出之前顯示調用pthread_join函數,該函數會等待參數id代表的線程退出,然后回收其資源,如果調用時目標線程還沒有運行結束,那么調用方(主進程)會被阻塞
當然,如果覺得這樣太麻煩,也可以使用pthread_detach函數主動分離線程,這樣,當線程運行結束后會由操作系統自動回收資源,不再需要主進程操心
還有一個常用的api是pthread_exit,用于主動結束當前線程
示例:若干線程并發對一個數進行自增操作
使用示例,創建10個線程對進程全局變量n做加法,每個線程加10000次
#include <unistd.h> #include <pthread.h> #include <sys/types.h>#include <iostream>long long int n = 0; void *thread_func(void* arg) {for(int i = 0; i < 10000; ++i)++n;::pthread_exit(nullptr); }int main() {for(int i = 0; i < 10; ++i){pthread_t pid;::pthread_create(&pid, nullptr, thread_func, nullptr); ::pthread_detach(pid);}::sleep(1); //等待所有線程正常退出std::cout << n << std::endl;return 0; }當然最后這個結果絕不可能是100000,要想保證正確性,需要互斥鎖協助。
C++11線程庫
簡單介紹了posix原生線程的使用,一方面用于復習,另一方面自然是為了引出主角。C++11引入線程庫std::thread,使得C++在語言級別上支持線程,雖然大家都說性能不咋地,但是用起來自然是方便許多。突出的幾個特點有
- 支持lambda,創建線程可以傳入lambda作為執行函數,太方便了有木有~
- 支持任意多個參數,由于C++模板支持可變參數列表,所以實現多參數傳遞還是蠻容易的
- 使用方便,各種函數都經過了良好設計,使用起來比posix不知道高到哪里去了
小插曲,介紹了這么多好處當然也要吐槽一下,編譯C++11線程庫居然要手動鏈接-lpthread庫….
創建線程的幾種方式
使用線程庫需要引入頭文件<thread>,有下面幾種方法創建線程
#include <iostream> #include <thread> #include <chrono> #include <functional>class ThreadTask { public:ThreadTask(int a, int b): a_(a), b_(b){ }void operator()(){std::cout << "hello " << std::this_thread::get_id() << std::endl;std::cout << "a + b = " << a_ + b_ << std::endl;std::this_thread::sleep_for(std::chrono::seconds(1));std::cout << "world " << std::this_thread::get_id() << std::endl;}private:int a_;int b_; };void func(int a) {std::cout << "hello " << std::this_thread::get_id() << std::endl;std::cout << a << std::endl;std::this_thread::sleep_for(std::chrono::seconds(1));std::cout << "world " << std::this_thread::get_id() << std::endl; }int main() {std::thread t1(func, 1); //接收一個函數指針和參數列表std::thread t2([]() {std::cout << "hello " << std::this_thread::get_id() << std::endl;std::this_thread::sleep_for(std::chrono::seconds(1));std::cout << "world " << std::this_thread::get_id() << std::endl;}); //接收lambdaThreadTask task(1, 2); std::thread t3(task); //接收函數對象t1.join(); //t1.detach();t2.join(); //t2.detach();t3.join(); //t3.detach();//std::this_thread::sleep_for(std::chrono::seconds(1));return 0; }需要注意的是,std::thread對象是不允許拷貝的,拷貝構造函數和拷貝賦值運算符被指定為delete
線程的移動語義
雖然不允許拷貝,但是std::thread是允許移動的
int main() {std::thread t1([]() {std::cout << "hello " << std::this_thread::get_id() << std::endl;std::this_thread::sleep_for(std::chrono::seconds(1));std::cout << "world " << std::this_thread::get_id() << std::endl;});std::thread t2(std::move(t4)); //移動構造函數std::thread t3;t3 = std::move(t2); //移動賦值運算符t3.join();return 0; }示例:利用std::thread實現并行的accumulate函數
標準庫std::accumulate函數用于對給定區間的元素依次運算,默認是加法,使用示例
#include <iostream> #include <algorithm> #include <vector>using namespace std;int main() {vector<int> v{1, 3, 5, 7, 9, 12};/* 輸出37,即所有元素的和 */std::cout << std::accumulate(v.begin(), v.end(), 0) << std::endl;/* 輸出5,即所有元素異或的結果 */std::cout << std::accumulate(v.begin(), v.end(), 0, bit_xor<int>()) << std::endl;return 0; }下面利用std::thread實現并行accumulate,即利用多線程的優勢同時計算不同區塊
首先是根據給定區間計算創建的線程個數,std::thread標準庫中提供了hardware_concurrency()函數,該函數返回當前計算機支持的并發線程數,通常是cpu核數,如果值無法計算則返回0。
在此之前,最好規定每個線程最小計算的元素個數,不然如果創建線程過多,導致每個線程計算的元素個數很少,那么創建線程帶來的開銷就會大于運算開銷,得不償失。所以可以規定每個線程最少計算20個元素
template <class Distance> auto getThreadNums(Distance count) {auto avaThreadNums = std::thread::hardware_concurrency();auto minCalNums = 20;/* 將count向上取整到20的整數倍,計算最大需要多少個線程 */auto maxThreadNums = ((count + (minCalNums - 1)) & (~(minCalNums - 1))) / minCalNums;/* 選擇二者中最合適的那個 */return avaThreadNums == 0 ? maxThreadNums : std::min(static_cast<int>(avaThreadNums), static_cast<int>(maxThreadNums)); }通過線程個數,就可以計算每個小區間負責的元素個數,從而將區間[front, last)拆分成若干個小區間
auto count = std::distance(first, last); int threadNums = getThreadNums(count); int blockSize = count / threadNums; //區間大小接下來的工作就是創建threadNums個線程,每個線程調用std::accumulate計算自己負責的區間結果,同時將結果保存,最后將每個區間的結果再求一次std::accumulate
template <class InputIt, class T> T parallel_accumulate(InputIt first, InputIt last, T init) {auto count = std::distance(first, last);int threadNums = getThreadNums(count);int blockSize = count / threadNums;std::vector<std::thread> threads;std::vector<T> results(threadNums);auto front = first;for(int i = 0; i < threadNums; ++i){auto back = front;if(i != threadNums - 1)std::advance(back, blockSize);elseback = last;threads.emplace_back([front, back, &results, i, init] { results[i] = std::accumulate(front, back, init); });front = back;}for(auto& th : threads)th.join();return std::accumulate(results.begin(), results.end(), init); }測試代碼為
#include <iostream> #include <algorithm> #include <vector> #include <thread> #include <future> #include <chrono> #include <functional> #include <random>#include "../../tinySTL/Profiler/profiler.h"/* parallel_accumulate的實現 */ ...int main() {std::vector<long long int> v(100000000);std::random_device rd;std::generate(v.begin(), v.end(), [&rd]() { return rd() % 1000; });tinystl::Profiler::ProfilerInstance::start(); auto result = parallel_accumulate(v.begin(), v.end(), 0);tinystl::Profiler::ProfilerInstance::finish(); tinystl::Profiler::ProfilerInstance::dumpDuringTime(); std::cout << result << std::endl;tinystl::Profiler::ProfilerInstance::start(); result = std::accumulate(v.begin(), v.end(), 0);tinystl::Profiler::ProfilerInstance::finish(); tinystl::Profiler::ProfilerInstance::dumpDuringTime(); std::cout << result << std::endl;return 0; }輸出結果
g++ thread.cpp -o thread -std=c++14 -lpthread -g //編譯 ./thread //執行 total 213.717 milliseconds -1584776879 total 776.649 milliseconds -1584776879雖然都溢出了,但是可以看出并行計算快很多
小結
C++11提供的線程庫使用起來比較方便,以前在學習多線程編程時一直使用的posix原生線程庫,剛剛接觸C++11時感覺方便很多,后面會繼續學習互斥鎖,條件變量的使用。
總結
以上是生活随笔為你收集整理的C++11学习笔记-----线程库std::thread的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 每天一道LeetCode-----后缀表
- 下一篇: C++11学习笔记-----互斥量以及条