博客原文:www.freehacker.cn
C++11中引入了多線程編程,一般教科書中都沒有涉及到這個概念,但是在工作中多線程卻又是必不可少的。本文會從最簡單的hello world入手,細述如何創建管理線程。
Hello World
經典的Hello World式開端。
#include <iostream>
#include <thread>
void hello()
{
std::cout << "Hello world" << std::endl;
}
int main()
{
std::thread t(hello);
t.join(); // 沒有這句話,會Debug Error的
return 0;
}
這段代碼很簡單,如果用過boost多線程編程,那么應該對這個了如指掌了。首先包含線程庫頭文件<thread>
,然后定義一個線程對象t,線程對象負責管理以hello()函數作為初始函數的線程,join()等待線程函數執行完成——這兒是阻塞的。
創建線程
上文中的經典hello world例子使用了最基本的線程創建方法,也是我們最常用的方法。std::thread對象的構造參數需要為Callable Object,可以是函數、函數對象、類的成員函數或者是Lambda表達式。接下來我們會給出這四種創建線程的方法。
以函數作為參數
上文中的Hello C++ Concurrency程序,就是最好的以函數為參數構造std::thread的例子,這里不再贅述。
以函數對象作為參數
函數對象利用了C++類的調用重載運算符,實現了該重載運算符的類對象可以當成函數一樣進行調用。如下例:
#include <iostream>
#include <thread>
class hello
{
public:
hello(){ }
void operator()()const
{
std::cout << "Hello world" << std::endl;
}
};
int main()
{
hello h;
std::thread t1(h);
t1.join();
return 0;
}
這里需要注意一點:如果需要直接傳遞臨時的函數對象,C++編譯器會將std::thread對象構造解析為函數聲明:
std::thread t2(hello()); // error, compile as std::thread t2(hello(*)());
std::thread t3((hello())); // ok
std::thread t4{ hello() }; // ok
t2.join(); // compile error: expression must have class type
t3.join(); // ok
t4.join(); // ok
以類的成員函數作為參數
為了作為std::thread的構造參數,類的成員函數名必須唯一,在下例中,如果world1()和world2()函數名都是world,則編譯出錯,這是因為名字解析發生在參數匹配之前。
#include <iostream>
#include <thread>
#include <string>
class hello
{
public:
hello(){ }
void world1()
{
std::cout << "Hello world" << std::endl;
}
void world2(std::string text)
{
std::cout << "Hello world, " << text << std::endl;
}
};
int main()
{
hello h;
std::thread t1(&hello::world1, &h);
std::thread t2(&hello::world2, &h, "lee");
t1.join();
t2.join();
return 0;
}
以lambda對象作為參數
#include <iostream>
#include <thread>
#include <string>
int main()
{
std::thread t([](std::string text){
std::cout << "hello world, " << text << std::endl;
}, "lee");
t.join();
return 0;
}
創建線程對象時需要切記,使用一個能訪問局部變量的函數去創建線程是一個糟糕的注意。
等待線程
join()等待線程完成,只能對一個線程對象調用一次join(),因為調用join()的行為,負責清理線程相關內容,如果再次調用,會出現Runtime Error
。
std::thread t([](){
std::cout << "hello world" << std::endl;
});
t.join(); // ok
t.join(); // runtime error
if(t.joinable())
{
t.join(); // ok
}
對join()的調用,需要選擇合適的調用時機。如果線程運行之后父線程產生異常,在join()調用之前拋出,就意味著這次調用會被跳過。解決辦法是,在無異常的情況下使用join()——在異常處理過程中調用join()。
#include <iostream>
#include <thread>
#include <string>
int main()
{
std::thread t([](std::string text){
std::cout << "hello world, " << text << std::endl;
}, "lee");
try
{
throw std::exception("test");
}
catch (std::exception e)
{
std::cout << e.what() << std::endl;
t.join();
}
if (t.joinable())
{
t.join();
}
return 0;
}
上面并非解決這個問題的根本方法,如果其他問題導致程序提前退出,上面方案無解,最好的方法是所謂的RAII。
#include <iostream>
#include <thread>
#include <string>
class thread_guard
{
public:
explicit thread_guard(std::thread &_t)
: t(std::move(_t))
{
if(!t.joinable())
throw std::logic_error("No Thread");
}
~thread_guard()
{
if (t.joinable())
{
t.join();
}
}
thread_guard(thread_guard const&) = delete;
thread_guard& operator=(thread_guard const &) = delete;
private:
std::thread t;
};
void func()
{
thread_guard guard(std::thread([](std::string text){
std::cout << "hello world, " << text << std::endl;
}, "lee"));
try
{
throw std::exception("test");
}
catch (...)
{
throw;
}
}
int main()
{
try
{
func();
}
catch (std::exception e)
{
std::cout << e.what() << std::endl;
}
return 0;
}
分離線程
detach()將子線程和父線程分離。分離線程后,可以避免異常安全問題,即使線程仍在后臺運行,分離操作也能確保std::terminate在std::thread對象銷毀時被調用。
通常稱分離線程為守護線程(deamon threads),這種線程的特點就是長時間運行;線程的生命周期可能會從某一個應用起始到結束,可能會在后臺監視文件系統,還有可能對緩存進行清理,亦或對數據結構進行優化。
#include <iostream>
#include <thread>
#include <string>
#include <assert.h>
int main()
{
std::thread t([](std::string text){
std::cout << "hello world, " << text << std::endl;
}, "lee");
if (t.joinable())
{
t.detach();
}
assert(!t.joinable());
return 0;
}
上面的代碼中使用到了joinable()
函數,不能對沒有執行線程的std::thread對象使用detach(),必須要使用joinable()函數來判斷是否可以加入或分離。
線程傳參
正常的線程傳參是很簡單的,但是需要記住下面一點:默認情況下,即使我們線程函數的參數是引用類型,參數會先被拷貝到線程空間,然后被線程執行體訪問。上面的線程空間為線程能夠訪問的內部內存。我們來看下面的例子:
void f(int i,std::string const& s);
std::thread t(f,3,”hello”);
即使f的第二個參數是引用類型,字符串字面值"hello"還是被拷貝到線程t空間內,然后被轉換為std::string類型。在上面這種情況下不會出錯,但是在下面這種參數為指向自動變量的指針的情況下就很容易出錯。
void f(int i,std::string const& s);
void oops(int some_param)
{
char buffer[1024];
sprintf(buffer, "%i",some_param);
std::thread t(f,3,buffer);
t.detach();
}
在這種情況下,指針變量buffer將會被拷貝到線程t空間內,這個時候很可能函數oops結束了,buffer還沒有被轉換為std::string,這個時候就會導致未定義行為。解決方案如下:
void f(int i,std::string const& s);
void not_oops(int some_param)
{
char buffer[1024];
sprintf(buffer,"%i",some_param);
std::thread t(f,3,std::string(buffer));
t.detach();
}
由于上面所說,進程傳參時,參數都會被進行一次拷貝,所以即使我們將進程函數參數設為引用,也只是對這份拷貝的引用。我們對參數的操作并不會改變其傳參之前的值。看下面例子:
void update_data_for_widget(widget_id w,widget_data& data);
void oops_again(widget_id w)
{
widget_data data;
std::thread t(update_data_for_widget,w,data);
display_status();
t.join();
process_widget_data(data);
}
線程t執行完成之后,data的值并不會有所改變,process_widget_data(data)函數處理的就是一開始的值。我們需要顯示的聲明引用傳參,使用std::ref包裹需要被引用傳遞的參數即可解決上面問題:
void update_data_for_widget(widget_id w,widget_data& data);
void oops_again(widget_id w)
{
widget_data data;
std::thread t(update_data_for_widget,w,std::ref(data));
display_status();
t.join();
process_widget_data(data);
}
對于可以移動不可拷貝的參數,譬如std::unqiue_ptr對象,如果源對象是臨時的,移動操作是自動執行的;如果源對象是命名變量,必須顯式調用std::move函數。
void process_big_object(std::unique_ptr<big_object>);
std::unique_ptr<big_object> p(new big_object);
p->prepare_data(42);
std::thread t(process_big_object,std::move(p));
轉移線程所有權
std::thread是可移動的,不可拷貝。在std::thread對象之間轉移線程所有權使用sd::move函數。
void some_function();
void some_other_function();
std::thread t1(some_function); // 1
std::thread t2=std::move(t1); // 2
t1=std::thread(some_other_function); // 3 臨時對象會隱式調用std::move轉移線程所有權
std::thread t3; // 4
t3=std::move(t2); // 5
t1=std::move(t3); // 6 賦值操作將使程序崩潰
t1.detach();
t1=std::move(t3); // 7 ok
這里需要注意的是臨時對象會隱式調用std::move轉移線程所有權,所以t1=std::thread(some_other_function);不需要顯示調用std::move。如果需要析構thread對象,必須等待join()返回或者是detach(),同樣,如果需要轉移線程所有權,必須要等待接受線程對象的執行函數完成,不能通過賦一個新值給std::thread對象的方式來"丟棄"一個線程。第6點中,t1仍然和some_other_function聯系再一次,所以不能直接轉交t3的所有權給t1。
std::thread支持移動,就意味著線程的所有權可以在函數外進行轉移。
std::thread f()
{
void some_function();
return std::thread(some_function);
}
std::thread g()
{
void some_other_function(int);
std::thread t(some_other_function,42);
return t;
}
當所有權可以在函數內部傳遞,就允許std::thread實例可作為參數進行傳遞。
void f(std::thread t);
void g()
{
void some_function();
f(std::thread(some_function));
std::thread t(some_function);
f(std::move(t));
}
利用這個特性,我們可以實現線程對象的RAII封裝。
class thread_guard
{
public:
explicit thread_guard(std::thread &_t)
: t(std::move(_t))
{
if (!t.joinable())
throw std::logic_error("No Thread");
}
~thread_guard()
{
if (t.joinable())
{
t.join();
}
}
thread_guard(thread_guard const&) = delete;
thread_guard& operator=(thread_guard const &) = delete;
private:
std::thread t;
};
struct func;
void f() {
int some_local_state;
scoped_thread t(std::thread(func(some_local_state)));
do_something_in_current_thread();
}
利用線程可以轉移的特性我們可以用容器來集中管理線程,看下面代碼:
void do_work(unsigned id);
void f() {
std::vector<std::thread> threads;
for(unsigned i=0;i<20;++i)
{
threads.push_back(std::thread(do_work,i));
}
std::for_each(threads.begin(),threads.end(),
std::mem_fn(&std::thread::join));
}
線程相關
線程數量
std::thread::hardware_concurrency()函數返回一個程序中能夠同時并發的線程數量,在多核系統中,其一般是核心數量。但是這個函數僅僅是一個提示,當系統信息無法獲取時,函數會返回0??聪旅娌⑿刑幚淼睦樱?/p>
識別線程
線程標識類型是std::thread::id,可以通過兩種方式進行檢索。
- 通過調用std::thread對象的成員函數get_id()來直接獲取。
- 當前線程中調用std::this_thread::get_id()也可以獲得線程標識。
上面的方案和線程sleep很相似,使用上面一樣的格式,get_id()函數替換成sleep()函數即可。
std::thread::id對象可以自由的拷貝和對比:
- 如果兩個對象的std::thread::id相等,那它們就是同一個線程,或者都“沒有線程”。
- 如果不等,那么就代表了兩個不同線程,或者一個有線程,另一沒有。
std::thread::id實例常用作檢測特定線程是否需要進行一些操作,這常常用在某些線程需要執行特殊操作的場景,我們必須先要找出這些線程。