線程可以共享進程的內存空間,線程擁有自己獨立內存。
關於參數的傳遞,std::thread的構造函數只會單純的復制傳入的變量,特別需要注意的是傳遞引用時,傳入的是值的副本,也就是說子線程中的修改影響不了主線程中的值。
值傳遞
主線程中的值,被拷貝一份傳到了子線程中。
1 #include <iostream> 2 #include <thread> 3 4 using namespace std; 5 6 void test(int ti, int tj) 7 { 8 cout << "子線程開始" << endl; 9 //ti的內存地址0x0055f69c {4},tj的內存地址0x0055f6a0 {5} 10 cout << ti << " " << tj << endl; 11 cout << "子線程結束" << endl; 12 return; 13 } 14 15 16 int main() 17 { 18 cout << "主線程開始" << endl; 19 //i的內存地址0x001efdfc {4},j的內存地址0x001efdf0 {5} 20 int i = 4, j = 5; 21 thread t(test, i, j); 22 t.join(); 23 cout << "主線程結束!" << endl; 24 return 0; 25 }
傳引用
從下面的運行結果,可以看出,即使是用引用來接收傳的值,也是會將其拷貝一份到子線程的獨立內存中,這一點與我們編寫普通程序時不同。這是因為線程的創建屬於函數式編程,所以為了傳引用C++中才引入了std::ref()。關於std::ref()。
1 #include <iostream> 2 #include <thread> 3 4 using namespace std; 5 6 class A{ 7 public: 8 int ai; 9 A (int i): ai(i) { } 10 }; 11 12 //這種情況必須在引用前加const,否則會出錯。目前本人的覺得可能是因為臨時對象具有常性 13 void test(const int &ti, const A &t) 14 { 15 cout << "子線程開始" << endl; 16 //ti的內存地址0x0126d2ec {4},t.ai的內存地址0x0126d2e8 {ai=5 } 17 cout << ti << " " << t.ai << endl; 18 cout << "子線程結束" << endl; 19 return; 20 } 21 22 23 int main() 24 { 25 cout << "主線程開始" << endl; 26 //i的內存地址0x010ff834 {4},a的內存地址0x010ff828 {ai=5 } 27 int i = 4; 28 A a = A(5); 29 thread t(test, i, a); 30 t.join(); 31 cout << "主線程結束!" << endl; 32 return 0; 33 }
那么如果我們真的需要像一般程序那樣傳遞引用呢,即在子線程中的修改能夠反映到主線程中。此時需要使用std::ref()。但是注意如果我們會在子線中改變它,此時用於接收ref()的那個參數前不能加const。關於C++多線程中的參數傳引用問題,我目前只是記住了這個現象,關於原理還需后期研究源碼繼續學習。
1 #include <iostream> 2 #include <thread> 3 4 using namespace std; 5 6 class A { 7 public: 8 int ai; 9 A(int i) : ai(i) { } 10 }; 11 12 //接收ref()的那個參數前不能加const,因為我們會改變那個值 13 void test(int& ti, const A& t) 14 { 15 cout << "子線程開始" << endl; 16 cout << ti << " " << t.ai << endl; 17 ti++; 18 cout << "子線程結束" << endl; 19 return; 20 } 21 22 23 int main() 24 { 25 cout << "主線程開始" << endl; 26 int i = 4; 27 A a = A(5); 28 thread t(test, ref(i), a); 29 t.join(); 30 cout << "i改變:" << i << endl; 31 cout << "主線程結束!" << endl; 32 return 0; 33 }
傳入類對象時,使用引用來接收比用值接收更高效。
1 #include <iostream> 2 #include <thread> 3 4 using namespace std; 5 6 class A { 7 public: 8 int ai; 9 A (int i) : ai(i) 10 { 11 cout << "構造" << this << endl; 12 } 13 14 A (const A& a) :ai(a.ai) { 15 cout << "拷貝構造" << this << endl; 16 } 17 18 ~A() 19 { 20 cout << "析構" << this << endl; 21 } 22 }; 23 24 //void test(const A a) 25 void test(const A& a) 26 { 27 cout << "子線程開始" << endl; 28 cout << "子線程結束" << endl; 29 return; 30 } 31 32 33 int main() 34 { 35 cout << "主線程開始" << endl; 36 int i = 4; 37 thread t(test, A(i)); 38 t.join(); 39 cout << "主線程結束!" << endl; 40 return 0; 41 }
傳指針
從下面的運行結果,可以看出,主線程和子線程中的指針都是指向同一塊內存。所以在這種情況下會有一個陷阱,如果使用detach(),則當主線程崩潰或者正常結束后,該塊內存被回收,若此時子線程沒有結束,那么子線程中指針的訪問將未定義,程序會出錯。
1 #include <iostream> 2 #include <thread> 3 4 using namespace std; 5 6 7 void test(char *p) 8 { 9 cout << "子線程開始" << endl; 10 //0x004ffeb4 "hello" 11 cout << p << endl; 12 cout << "子線程結束" << endl; 13 return; 14 } 15 16 17 int main() 18 { 19 cout << "主線程開始" << endl; 20 //0x004ffeb4 "hello" 21 char s[] = "hello"; 22 thread t(test, s); 23 t.join(); 24 cout << "主線程結束!" << endl; 25 return 0; 26 }
傳臨時對象
用臨時變量作為實參時,會更高效,由於臨時變量會隱式自動進行移動操作,這就減少了整體構造函數的調用次數。而一個命名變量的移動操作就需要std::move()。
1 #include <iostream> 2 #include <thread> 3 4 using namespace std; 5 6 class A { 7 public: 8 int ai; 9 A (int i) : ai(i) 10 { 11 cout << "構造" << this << endl; 12 } 13 14 A (const A& a) :ai(a.ai) { 15 cout << "拷貝構造" << this << endl; 16 } 17 18 ~A() 19 { 20 cout << "析構" << this << endl; 21 } 22 }; 23 24 void test(const A& a) 25 { 26 cout << "子線程開始" << endl; 27 cout << "子線程結束" << endl; 28 return; 29 } 30 31 32 int main() 33 { 34 cout << "主線程開始" << endl; 35 int i = 4; 36 thread t(test, A(i)); 37 t.join(); 38 cout << "主線程結束!" << endl; 39 return 0; 40 }
總結
1、使用引用和指針是要注意;
2、對於內置簡單類型,建議傳值;
3、對於類對象,建議使用引用來接收,以為使用引用會只會構造兩次,而傳值會構造三次;
4、在detach下要避免隱式轉換,因為此時子線程可能還來不及轉換主線程就結束了,應該在構造線程時,用參數構造一個臨時對象傳入。