清單2.4中,向std::thread
構(gòu)造函數(shù)中的可調(diào)用對象,或函數(shù)傳遞一個參數(shù)很簡單。需要注意的是,默認參數(shù)要拷貝到線程獨立內(nèi)存中,即使參數(shù)是引用的形式,也可以在新線程中進行訪問。再來看一個例子:
void f(int i, std::string const& s);
std::thread t(f, 3, "hello");
代碼創(chuàng)建了一個調(diào)用f(3, "hello")的線程。注意,函數(shù)f需要一個std::string
對象作為第二個參數(shù),但這里使用的是字符串的字面值,也就是char const *
類型。之后,在線程的上下文中完成字面值向std::string
對象的轉(zhuǎn)化。需要特別要注意,當指向動態(tài)變量的指針作為參數(shù)傳遞給線程的情況,代碼如下:
void f(int i,std::string const& s);
void oops(int some_param)
{
char buffer[1024]; // 1
sprintf(buffer, "%i",some_param);
std::thread t(f,3,buffer); // 2
t.detach();
}
這種情況下,buffer②是一個指針變量,指向本地變量,然后本地變量通過buffer傳遞到新線程中②。并且,函數(shù)有很有可能會在字面值轉(zhuǎn)化成std::string
對象之前崩潰(oops),從而導致一些未定義的行為。并且想要依賴隱式轉(zhuǎn)換將字面值轉(zhuǎn)換為函數(shù)期待的std::string
對象,但因std::thread
的構(gòu)造函數(shù)會復制提供的變量,就只復制了沒有轉(zhuǎn)換成期望類型的字符串字面值。
解決方案就是在傳遞到std::thread
構(gòu)造函數(shù)之前就將字面值轉(zhuǎn)化為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)); // 使用std::string,避免懸垂指針
t.detach();
}
還可能遇到相反的情況:期望傳遞一個引用,但整個對象被復制了。當線程更新一個引用傳遞的數(shù)據(jù)結(jié)構(gòu)時,這種情況就可能發(fā)生,比如:
void update_data_for_widget(widget_id w,widget_data& data); // 1
void oops_again(widget_id w)
{
widget_data data;
std::thread t(update_data_for_widget,w,data); // 2
display_status();
t.join();
process_widget_data(data); // 3
}
雖然update_data_for_widget①的第二個參數(shù)期待傳入一個引用,但是std::thread
的構(gòu)造函數(shù)②并不知曉;構(gòu)造函數(shù)無視函數(shù)期待的參數(shù)類型,并盲目的拷貝已提供的變量。當線程調(diào)用update_data_for_widget函數(shù)時,傳遞給函數(shù)的參數(shù)是data變量內(nèi)部拷貝的引用,而非數(shù)據(jù)本身的引用。因此,當線程結(jié)束時,內(nèi)部拷貝數(shù)據(jù)將會在數(shù)據(jù)更新階段被銷毀,且process_widget_data將會接收到?jīng)]有修改的data變量③??梢允褂?code>std::ref將參數(shù)轉(zhuǎn)換成引用的形式,從而可將線程的調(diào)用改為以下形式:
std::thread t(update_data_for_widget,w,std::ref(data));
在這之后,update_data_for_widget就會接收到一個data變量的引用,而非一個data變量拷貝的引用。
如果你熟悉std::bind
,就應該不會對以上述傳參的形式感到奇怪,因為std::thread
構(gòu)造函數(shù)和std::bind
的操作都在標準庫中定義好了,可以傳遞一個成員函數(shù)指針作為線程函數(shù),并提供一個合適的對象指針作為第一個參數(shù):
class X
{
public:
void do_lengthy_work();
};
X my_x;
std::thread t(&X::do_lengthy_work,&my_x); // 1
這段代碼中,新線程將my_x.do_lengthy_work()作為線程函數(shù);my_x的地址①作為指針對象提供給函數(shù)。也可以為成員函數(shù)提供參數(shù):std::thread
構(gòu)造函數(shù)的第三個參數(shù)就是成員函數(shù)的第一個參數(shù),以此類推(代碼如下,譯者自加)。
class X
{
public:
void do_lengthy_work(int);
};
X my_x;
int num(0);
std::thread t(&X::do_lengthy_work, &my_x, num);
有趣的是,提供的參數(shù)可以移動,但不能拷貝。"移動"是指:原始對象中的數(shù)據(jù)轉(zhuǎn)移給另一對象,而轉(zhuǎn)移的這些數(shù)據(jù)就不再在原始對象中保存了(譯者:比較像在文本編輯的"剪切"操作)。std::unique_ptr
就是這樣一種類型(譯者:C++11中的智能指針),這種類型為動態(tài)分配的對象提供內(nèi)存自動管理機制(譯者:類似垃圾回收)。同一時間內(nèi),只允許一個std::unique_ptr
實現(xiàn)指向一個給定對象,并且當這個實現(xiàn)銷毀時,指向的對象也將被刪除。移動構(gòu)造函數(shù)(move constructor)和移動賦值操作符(move assignment operator)允許一個對象在多個std::unique_ptr
實現(xiàn)中傳遞(有關(guān)"移動"的更多內(nèi)容,請參考附錄A的A.1.1節(jié))。使用"移動"轉(zhuǎn)移原對象后,就會留下一個空指針(NULL)。移動操作可以將對象轉(zhuǎn)換成可接受的類型,例如:函數(shù)參數(shù)或函數(shù)返回的類型。當原對象是一個臨時變量時,自動進行移動操作,但當原對象是一個命名變量,那么轉(zhuǎn)移的時候就需要使用std::move()
進行顯示移動。下面的代碼展示了std::move
的用法,展示了std::move
是如何轉(zhuǎn)移一個動態(tài)對象到一個線程中去的:
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
的構(gòu)造函數(shù)中指定std::move(p)
,big_object對象的所有權(quán)就被首先轉(zhuǎn)移到新創(chuàng)建線程的的內(nèi)部存儲中,之后傳遞給process_big_object函數(shù)。
標準線程庫中和std::unique_ptr
在所屬權(quán)上有相似語義類型的類有好幾種,std::thread
為其中之一。雖然,std::thread
實例不像std::unique_ptr
那樣能占有一個動態(tài)對象的所有權(quán),但是它能占有其他資源:每個實例都負責管理一個執(zhí)行線程。執(zhí)行線程的所有權(quán)可以在多個std::thread
實例中互相轉(zhuǎn)移,這是依賴于std::thread
實例的可移動且不可復制性。不可復制保性證了在同一時間點,一個std::thread
實例只能關(guān)聯(lián)一個執(zhí)行線程;可移動性使得程序員可以自己決定,哪個實例擁有實際執(zhí)行線程的所有權(quán)。