頭文件<thread>
創建thread
對象
thread
有三個構造函數,分別是默認構造函數,複製構造函數,和帶參數的構造函數.
創建和使用一個thread實例類似如下:
void print()
{
cout << "新線程啓動" << endl;
cout << "新線程結束" << endl;
}
int main()
{
thread t(print);
t.join();
cout << "主線程結束\n";
cout << endl;
}
其中第一個參數是可調用對象(函數、成員函數、函數對象、lambda)
當創建出一個thread對象時,C++就自動將它儘可能啓動於一個新線程,如果沒有成功,則拋出異常std::system_error
.
成員函數join()
的使用方法
join
中文直譯意思是加入、結合、連接.
顧名思義,使用這個成員函數,子線程會和主線程匯合,也就是說主線程會等待子線程結束,在子線程結束之前,主線程不會結束.
使用Join
,我們可以確保子線程能夠完全的運行,如果主線程走的快,主線程就會等待子線程的完畢,最後主線程和子線程都能運行完畢.
成員函數detach()
的使用方法
detach
中文直譯意思是分離、分開.
顧名思義,我們各走各的,你走你的,我走我的。也就是說主線程不會等待子線程,即使主線程結束的時候子線程還沒有結束。
一但使用detach
,與主線程相關的thread對象就會失去與主線程的關聯,此時這個子線程就會駐留在後臺運行,相當於被C++運行時庫接管,當這個子線程執行完畢後,由運行時庫負責清理該線程相關的資源.
因此使用detach會帶來很多的問題,比如
:
void print()
{
cout << "子線程1" << endl;
cout << "子線程2" << endl;
cout << "子線程3" << endl;
cout << "子線程4" << endl;
cout << "子線程5" << endl;
cout << "子線程6" << endl;
cout << "子線程7" << endl;
cout << "子線程8" << endl;
cout << "子線程9" << endl;
}
int main()
{
thread t(print);
t.detach();
cout << "主線程1\n";
cout << "主線程2\n";
cout << "主線程3\n";
cout << "主線程4\n";
cout << "主線程5\n";
cout << "主線程6\n";
cout << endl;
}
可以看到每次運行都會有不同的結果
…
…
傳遞臨時對象作爲線程參數帶來的問題
當可調用對象使用引用時,還會帶來其他的結果:
class A
{
public:
int #
public:
A(int &i) :num(i) {}
void operator()()
{
cout << num << endl;
cout << num << endl;
cout << num << endl;
}
};
int main()
{
int n = 1;
A a(n);
thread t(a);
t.detach();
cout << "hello world" << endl;
return 0;
}
可以看到,當主線程結束的時候,此時引用的n已經被消耗了,此時訪問引用就會帶來無法預料的結果.
還有個疑問:當主線程結束時,上面的a對象不是已經被銷燬了嗎?爲什麼還能調用?
實際上,子線程調用的對象實際上是複製到子線程中的,當主線程結束以後原來的對象確實已經被銷燬了,但是複製的對象沒有消耗,還是能使用的。
我們可以使用一個例子來論證一下:
class A
{
public:
int #
public:
A(int &i) :num(i) { cout << "帶參構造函數調用" << endl; }
A(const A& other) :num(other.num) { cout << "複製構造函數調用" << endl; }
~A()
{
cout << "析構函數調用" << endl;
}
void operator()()
{
cout << num << endl;
cout << num << endl;
cout << num << endl;
}
};
int main()
{
int n = 1;
A a(n);
thread t(a);
t.join();
cout << "hello world" << endl;
return 0;
}
當調用參數時,會出現你想象不到的結果:
void print(const int & i, char * p)
{
cout << i << endl;
cout << p << endl;
return;
}
int main()
{
int n = 1;
int& m = n;
char c[] = "test!";
thread t(print, m, c);
t.detach();
cout << "hello world" << endl;
return 0;
}
得到的地址如下:
&n 0x00affa2c {1}
&m 0x00affa2c {1}
&i 0x00f0f444 {1}
c 0x00f3f978 "test!"
p 0x00f3f978 "test!"
可以知道i並不是n的引用,實際上是值傳遞,即使主線程detach了子線程,那麼子線程中用i值仍然是安全的。
但是p和c卻是相同的地址,當主線程完成後 c的內存就被釋放,這個時候訪問p就會有很大的危險
如果換成string呢會怎麼樣?
void print(const int & i, const string & p)
{
cout << i << endl;
cout << p.c_str() << endl;
return;
}
int main()
{
int n = 1;
int& m = n;
char c[] = "test!";
thread t(print, m, c);
t.detach();
cout << "hello world" << endl;
return 0;
}
c 0x0056fd20 "test!" char[6]
&p 0x0079f7b0 "test!" const std::basic_string<char,std::char_traits<char>,std::allocator<char> > *
可以看到用string得到的結果是不一樣的,看上去string沒有引用主線程的c,好像安全了。問題是真的安全了?
有一個問題很有可能被忽略了,c是什麼時候轉換成string p的呢
?很有可能c被回收瞭然後還沒有進行轉換
解決辦法:使用臨時對象,避免隱式轉換:
thread t(print, m, string(c));
驗證如下:
class A
{
public:
int num;
public:
A(int i) :num(i) { cout << "帶參構造函數調用" << endl; }
A(const A& other) :num(other.num) { cout << "複製構造函數調用" << endl; }
~A()
{
cout << "析構函數調用" << endl;
}
void operator()()
{
cout << num << endl;
cout << num << endl;
cout << num << endl;
}
};
void print(const int & i, const A & p)
{
cout << i << endl;
cout << p.num << endl;
return;
}
int main()
{
int n = 1;
int& m = n;
char c[] = "test!";
thread t(print, m, A(10));
t.detach();
cout << "hello world" << endl;
return 0;
}
總結:
1.如果傳遞int這種簡單類型,建議都是按值傳遞,不要用引用.
2.如果傳遞類對象,避免隱式類型轉換,在創建線程這一行就構建出臨時對象,然後參數使用引用,不然會在多構造出一個對象。
3.避免麻煩,儘量別使用detach
線程ID
線程id是一個數字,每一個線程都有自己的id.
使用std::this_thread::get_id()
來獲取,
class A
{
private:
int m;
public:
A(int a) :m(a) { cout << "帶參數構造函數執行,線程ID是: " << this_thread::get_id() << endl; }
A(const A& other) :m(other.m) {
cout << "複製構造函數執行,線程ID是: " << this_thread::get_id() << endl;
}
A() {cout << "默認構造函數執行,線程ID是: " << this_thread::get_id() << endl; }
~A() { cout << "析構函數執行,線程ID是: " << this_thread::get_id() << endl; }
};
void print(const A & a)
{
cout << "子線程print的參數地址是:" << &a << ",子線程id是: " << this_thread::get_id() << endl;
}
int main()
{
cout << "主線程id是: " << this_thread::get_id() << endl;
int n = 1;
thread m(print, n);
m.join();
}
可以看到A對象是在子線程中構造的,如果使用detach,就會出現主線程結束而子線程還沒構造對象的情況,此時調用n就會出現不可預料的結果
##如果換成臨時對象呢?
thread m(print, A(n));
可以看到主線程進行了帶參構造和複製構造,就不會有上面的問題
std::ref
函數
class A
{
public:
mutable int m;
public:
A(int a) :m(a) { cout << "帶參數構造函數執行,線程ID是: " << this_thread::get_id() << endl; }
A(const A& other) :m(other.m) {
cout << "複製構造函數執行,線程ID是: " << this_thread::get_id() << endl;
}
A() {cout << "默認構造函數執行,線程ID是: " << this_thread::get_id() << endl; }
~A() { cout << "析構函數執行,線程ID是: " << this_thread::get_id() << endl; }
};
void print(const A & a)
{
a.m = 100;
cout << "子線程print的參數地址是:" << &a << ",子線程id是: " << this_thread::get_id() << endl;
}
int main()
{
cout << "主線程id是: " << this_thread::get_id() << endl;
int n = 1;
A a(n);
thread m(print, a);
m.join();
}
// &a 0x004ffe24 {m=1 } A *
// &a 0x0081fc50 {m=100 } const A *
可以看到即使傳遞是引用,子線程的修改不會對主線程造成任何影響。如果真的想修改需要使用std::ref函數
void print(const A & temp)
{
temp.m = 100;
cout << "子線程print的參數地址是:" << &temp << ",子線程id是: " << this_thread::get_id() << endl;
}
int main()
{
cout << "主線程id是: " << this_thread::get_id() << endl;
int n = 1;
A a(n);
thread m(print, ref(a));
m.join();
}
// &temp 0x004ff9c0 {m=100 } const A *
// &a 0x004ff9c0 {m=100 } A *
智能指針做參數
可以看到無法編譯通過,解決辦法:使用std::move
注意:這種情況不要使用detach了,會有危險。
…
…
成員函數做線程參數
class A
{
public:
mutable int m;
public:
A(int a) :m(a) { cout << "帶參數構造函數執行,線程ID是: " << this_thread::get_id() << endl; }
A(const A& other) :m(other.m) {
cout << "複製構造函數執行,線程ID是: " << this_thread::get_id() << endl;
}
A() {cout << "默認構造函數執行,線程ID是: " << this_thread::get_id() << endl; }
~A() { cout << "析構函數執行,線程ID是: " << this_thread::get_id() << endl; }
void print(int n)
{
cout << "子線程print執行 " << this << " 線程id = " << this_thread::get_id() << endl;
}
};
void print(unique_ptr<int>p)
{
}
int main()
{
cout << "主線程id是: " << this_thread::get_id() << endl;
A myobj(10);
thread t(&A::print, myobj, 15);
t.join();
}