C++ 多線程學習筆記(3):線程傳參的進一步分析

  • 根據前一篇文章的分析:C++ 多線程學習筆記(2):線程啓動、結束、創建線程方法:C++多線程中使用detach可能導致問題。
  • 假設在構造子線程時,傳入參數引用了主線程的變量,而且子線程創建後調用detach() 了,如果主線程先執行完,這個被引用的變量就會被回收,而此時子線程(沒執行完)仍在引用這塊內存空間,會導致不可預料的結果
  • 這裏針對這個問題進行進一步分析

1. 傳遞臨時對象作爲線程參數

(1)要避免的陷阱

  • 範例
  #include <iostream>
  #include <thread>
  using namespace std;
  
  //子線程起始函數
  void myprint(const int &i, char *pmybuf)
  {
  	cout << i << endl;
  	cout << pmybuf << endl;
  	return;
  }
  
  int main()
  {
  	int mvar = 1;
  	int &mvary = mvar;
  	char mybuf[] = "this is a test!";
  
  	thread mytobj(myprint, mvar, mybuf);
  	mytobj.join();
  	cout << "主線程" << endl;
  
  	return 0;
  }
  
  /*------運行結果--------
  1
  this is a test!
  主線程
  */
  • 上面的程序沒啥問題,但如果把join換成detach,就會出現問題
  #include <iostream>
  #include <thread>
  using namespace std;
  
  //子線程起始函數
  void myprint(const int &i, char *pmybuf)
  {
  	cout << i << endl;
  	cout << pmybuf << endl;
  	return;
  }
  
  int main()
  {
  	int mvar = 1;
  	int &mvary = mvar;
  	char mybuf[] = "this is a test!";
  
  	thread mytobj(myprint, mvar, mybuf);
  	mytobj.detach();
  	cout << "主線程" << endl;
  
  	return 0;
  }
  • debug可以發現,實參 mvar 的地址和 myprint 函數形參的 &i 的地址不同,說明thread構造函數內部應當是進行了複製,&i這個引用是假的引用,本質還是把 mvar 的值複製過去的

    • 因此,即使主線程中detach了子線程,子線程中的引用還是安全的
    • 但是,依然不推薦這樣寫!
  • debug可以發現,實參 mybuf 的地址和 myprint 函數形參的 *pmybuf 的地址相同,說明指針傳遞是傳地址的

    • 因此,如果主線程中detach了子線程,子線程中的指針是不安全的,如果主線程先於子線程結束並撤銷內存,子線程會發生不可預測的結果
  • 針對上面發現的引用問題,我們可以做以下修改

  //子線程起始函數
  void myprint(const int i, const string &pmybuf)
  {
  	cout << i << endl;
  	cout << pmybuf.c_str() << endl;
  	return;
  }
  
  int main()
  {
  	int mvar = 1;
  	int &mvary = mvar;
  	char mybuf[] = "this is a test!";
  
  	thread mytobj(myprint, mvar, mybuf);
  	mytobj.detach();
  	cout << "主線程" << endl;
  
  	return 0;
  }
  • 第一個形參變成傳值了,肯定沒問題

  • 第二個從指針變成string引用了,因爲實參是char數組,傳參時勢必要隱式構造一個string,然後引用的時候還會構造一個臨時string的副本(按上面的分析)。總之這樣就不會引用實參的 mybuf。debug也可驗證,實參的 mybuf 地址和形參的 pmybuf 地址不同,所以可以認爲這麼改就一定安全嗎?

  • 事實上,不行!!可以驗證,有可能會發生這種情況:主線程已經結束並被回收了,纔拿mybuf指針去轉string準備開始執行子線程的情況發生,這會使程序陷入不可預料中!

  • 最終修改版

  //子線程起始函數
  void myprint(const int i, const string &pmybuf)
  {
  	cout << i << endl;
  	cout << pmybuf.c_str() << endl;
  	return;
  }
  
  int main()
  {
  	int mvar = 1;
  	int &mvary = mvar;
  	char mybuf[] = "this is a test!";
  
  	thread mytobj(myprint, mvar, string(mybuf));	//這裏改成生成臨時string!
  	mytobj.detach();
  	cout << "主線程" << endl;
  
  	return 0;
  }
  • 這種方法可以保證安全,形參pmybuf引用了臨時string,之後thread 傳引用時內部又複製了一遍(上面分析了)。經過測試,可以保證主線程結束前臨時string一定生成。
  • 小結:
    • 當子線程的起始函數使用非基本類型的A類對象的引用作爲形參時,只要用臨時構造的A類對象作爲參數傳給thread構造函數(不能用隱式類型轉換),就可以保證一定能在主線程結束之前把子線程形參的這個引用的真實對象構造出來(先構造臨時對象,再拷貝一次),從而確保即使detach子線程也能安全運行。

(2)總結

  • 如果使用 detach 子線程

    • 若傳遞int這種簡單變量,建議都傳值

    • 如果傳遞類對象,避免隱式類型轉換。全部在thread構造函數調用時就構造臨時對象,在函數形參中用引用來接

  • 建議不使用detach,只用join,這樣就不存在局部變量失效導致線程對內存的非法引用問題

2. 臨時對象作爲線程參數(續)

(1)線程id概念

  • id是個數字,每個線程(不管主線程還是子線程)都有一個線程id
  • 不同的線程,線程id不同
  • 可以用std::this_thread::get_id()來獲取線程id

(2)臨時對象構造時間抓捕

  • 經過實驗發現
    • 傳遞類對象,使用隱式類型轉換時,臨時對象和拷貝對象都是在子線程構造的
    • 傳遞類對象,使用臨時對象傳參時,臨時對象和拷貝對象都是在主線程構造的
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章