C++11多執行緒thread引數傳遞問題
目錄
寫在前面
多執行緒在很多地方都是必須要掌握的方法,這裡先說一下,thread物件的引數傳遞問題
thread類的建構函式
thread() noexcept; //default constructor 其中noexcept表示函式不會丟擲異常,如果丟擲異常程式就會終止 template <class Fn, class... Args> explicit thread (Fn&& fn, Args&&... args); //initialization constructor explicit 表示不支援隱式轉換 thread (const thread&) = delete; //copy constructor delete表示不生成預設拷貝建構函式,並且可以禁止使用某個函式,也就表示不可以使用一個執行緒初始化另一個執行緒 thread (thread&& x) noexcept; //move constructor
join函式
join()函式有兩個作用,①、等待子執行緒執行完畢,主執行緒才結束執行;②、清理子執行緒相關的儲存器,是std::thread不再與子執行緒相關聯,釋放子執行緒中的資源
join函式需要考慮的問題是,什麼時候寫join函式,如果在使用join函式之前,程式因為異常問題導致終止,沒有呼叫join函式,導致記憶體洩露問題等等
detach函式
detach()函式,讓執行緒在後臺執行主,意味著執行緒不能與之產生直接互動,後臺執行緒的歸屬和控制都由C++執行時庫處理。
detach函式需要考慮的問題是,確保子執行緒中的引數必須為物件的複製,因為可能主執行緒退出導致臨時物件實現,子執行緒物件相繼實現,出現不可預料的問題。
thread中引數傳遞
正如上面的初始化建構函式,我們傳遞引數將引數依次放在初始化建構函式中能夠即可
#include <iostream> #include <thread> void foo(const int &x,char *mychar) { std::cout << &x << " " << &mychar << std::endl; std::cout << "正在執行的執行緒為:" << std::this_thread::get_id() << "執行緒的引數為: " << x <<" "<<mychar<< std::endl; return; } int main() { std::cout << "主執行緒的執行緒id為: " << std::this_thread::get_id() << std::endl; int x = 1; char mybuff[] = "This is a test"; std::cout << &x << " " << &mybuff << std::endl; std::thread second(foo, x, mybuff); second.join(); std::cout << "主執行緒執行結束" << std::endl; return 0; }
對於x,使用的是引用傳遞,但是數字物件地址在主執行緒和子執行緒中地址不同。要在子執行緒中共享資料x,即地址相同,必須使用std::ref進行修飾,下面再在類物件作為引數傳遞時會有更詳細說明
這樣可以將引數傳遞到子執行緒中,並且我們可以發現子執行緒中的字元物件地址和傳遞進入的字元地址是相同的,所以我們可以推斷指標這裡使用的是淺層拷貝,兩個指標指向的是同一個地址,所以這裡的字串時共享的,我們這裡可以這樣解決:
#include <iostream>
#include <string>
#include <thread>
void foo(const int &x,const std::string &mychar) //這裡將char*轉換為string,首先隱式轉換為string,然後將string複製拷貝到子執行緒中
{
//std::cout << &x << " " << &mychar << std::endl;
std::cout << "正在執行的執行緒為:" << std::this_thread::get_id() << "執行緒的引數為: " << x <<" "<<mychar<< std::endl;
return;
}
int main()
{
std::cout << "主執行緒的執行緒id為: " << std::this_thread::get_id() << std::endl;
int x = 1;
char mybuff[] = "This is a test";
//std::cout << &x << " " << &mybuff << std::endl;
std::thread second(foo, x, mybuff);
second.join();
std::cout << "主執行緒執行結束" << std::endl;
return 0;
}
這裡在子執行緒的函式引數中使用的是const string的引用,這樣就首先在函式中將char*隱式轉換為string,然後將string傳送到子執行緒中進行操作
但是如果使用detach函式,還是有可能會產生問題,因為不確定什麼時候進行飲食轉換,可能在主執行緒結束後,隱式轉換還沒有開始,mybuff就已經失效了,這樣依然會產生不可預料的結果。
解決方法:
在主執行緒中進行顯示轉換,這樣可以保證一定可以在主執行緒中進行顯示轉換:
std::thread second(foo, x, std::string(mybuff)); //只要在主執行緒中進行顯示轉換就可以解決上面所說的問題
下面我們就對上面的說法進行驗證:
#include <iostream>
#include <string>
#include <thread>
#include <mutex>
class A
{
public:
A(int _a = 0) :a(_a) //轉換函式
{
std::cout << "建構函式執行,執行他的執行緒為:" << std::this_thread::get_id() << std::endl;
}
A(const A& x) :a(x.a)
{
std::cout << "拷貝建構函式執行,執行他的執行緒為:" << std::this_thread::get_id() << std::endl;
}
~A()
{
std::cout << "解構函式執行,執行他的執行緒為:" << std::this_thread::get_id() << std::endl;
}
friend std::ostream &operator <<(std::ostream &os, const A &a); //因為ostream沒有拷貝建構函式,所以必須傳遞引用型別的ostream
//void operator()() //將類物件作為引數,需要過載運算子()
//{
// std::cout << "作為類引數物件正在執行,執行的執行緒為:" << std::this_thread::get_id() << std::endl;
// return;
//}
private:
int a;
};
std::ostream &operator <<(std::ostream &os, const A &a)
{
os << a.a << std::endl;
return os;
}
void foo(const int &x,const A &a)
{
std::cout << "子執行緒正在執行,執行緒id為:" << std::this_thread::get_id() << "執行緒的引數為: " << x << " " << a << std::endl;
return;
}
int main()
{
std::cout << "主執行緒的執行緒id為: " << std::this_thread::get_id() << std::endl;
int num = 5;
std::thread first(foo,num,num);
first.join();
std::cout << "主執行緒執行結束" << std::endl;
return 0;
}
這樣我們發現,隱式轉換是在子執行緒中進行的,但是我們只要增加顯示轉換後:
std::thread first(foo,num,A(num));
首先物件現在主執行緒中進行構造,並且將拷貝物件傳到子執行緒中,實現物件的分離,所以這樣即使在最後使用detcah函式,也不會早場引用已經析構的物件。
類物件作為引數
如果需要將類物件作為引數,需要在類中過載()運算子。栗子如下:
#include <iostream>
#include <thread>
class A
{
public:
A(int _a = 0) :a(_a)
{
std::cout << "建構函式執行,執行他的執行緒為:" << std::this_thread::get_id() << std::endl;
}
A(const A& x):a(x.a)
{
std::cout << "拷貝建構函式執行,執行他的執行緒為:" << std::this_thread::get_id() << std::endl;
}
~A()
{
std::cout << "解構函式執行,執行他的執行緒為:" << std::this_thread::get_id() << std::endl;
}
void operator()() //將類物件作為引數,需要過載運算子()
{
a--;
std::cout << "作為類引數物件正在執行,執行的執行緒為:" << std::this_thread::get_id() << "a的值為:"<< a <<std::endl;
return;
}
void print_a()
{
std::cout << "a的值為: " << a << std::endl;
}
private:
int a;
};
int main()
{
std::cout << "主執行緒的執行緒id為: " << std::this_thread::get_id() << std::endl;
A a(5);
a.print_a();
//std::thread first((A()));使用預設構造的未命名的變數,要在外面增阿基括號,將其解釋為物件
std::thread first(a);
first.join();
a.print_a();
std::cout << "主執行緒執行結束" << std::endl;
return 0;
}
輸出結果為:
可以看到,函式物件首先在主執行緒中做一次拷貝構造,然後將拷貝後的物件傳入執行緒中,在子執行緒中對物件進行修改也不會影響主執行緒中的物件
考慮一下:如果我們將first.join()換成first.detach(),這裡會不會因為主執行緒提前退出,物件a提前銷燬,對子執行緒中的物件造成影響?
正如我們上面解釋的,傳入子執行緒中的是在主執行緒中複製的物件,所以這裡我們可以使用detach和join作用是相同的。
如果要將主執行緒物件本身傳入子執行緒中,我們需要在傳入引數中新增std::ref(a)即可,這裡就必須要使用first.join(),防止出現上面所說的問題。
類中函式作為引數
#include <iostream>
#include <string>
#include <thread>
#include <list>
#include <mutex>
class MesProcess
{
public:
void Inmsglist()
{
for (int i = 0; i < 100000; i++)
{
std::cout << "Inmsglist執行緒正在執行,插入數字為:" << i << std::endl;
msglist.push_back(i);
}
}
void Outmsglist()
{
int command;
for (int i = 0; i < 100000; i++)
{
if (!msglist.empty())
{
command = msglist.front();
msglist.pop_front();
std::cout << "Outmsglist執行緒正在執行,取出數字為:" << command << std::endl;
}
else
{
std::cout << "Outmsglist執行緒正在執行,但是訊息佇列為空" << std::endl;
}
}
}
private:
std::list<int> msglist;
};
int main()
{
std::cout << "主執行緒的執行緒id為: " << std::this_thread::get_id() << std::endl;
MesProcess mpobj;
std::thread Outmsg_thread(&MesProcess::Outmsglist, &mpobj);
std::thread Inmsg_thread(&MesProcess::Inmsglist, &mpobj);
Inmsg_thread.join();
Outmsg_thread.join();
std::cout << "主執行緒執行結束" << std::endl;
return 0;
}
首先說明:因為程式中沒有對資料進行保護,所以一定會出現錯誤,出現錯誤的時間不一定
我們這一要說的是類中函式作為執行緒引數,我們這裡還需要傳遞一個類物件,因為我們知道在類中的函式存在一個預設引數,就是物件本身this,所以這裡要將物件傳遞進去。
參考書籍
《c++併發程式設計實戰》 Anthony Williams著
《c++primer puls第六版》
《C++11併發與多執行緒視訊課程》 視訊地址:http://edu.51cto.com/course/15287.html