天天看點

UNIX(多線程):08---線程傳參詳解,detach()陷阱,成員函數做線程函數

線程傳參詳解,detach()陷阱,成員函數做線程函數

傳遞臨時對象作為線程參數

【引例】

#include <iostream>
#include <string>
#include <thread>
using namespace std;
void myprint(const int& i, char* pmybuf ) {
cout << i << endl;
cout << pmybuf << endl;
return;
}
int main()
{
int val = 1;
int& val_y = val;
char buf[] = "This is a Test!";
thread mythread(myprint, val, buf);  //傳遞參數
mythread.join();
//主線程執行
std::cout << "主線程收尾" << std::endl;
return 0;
}      

要避免的陷阱(解釋1)

  • 如果上面使用detach,子線程和主線程分别執行,由于傳遞給myprint的是val的引用,如果主線程結束,會不會給子線程造成影響?
  • 答案是不會,雖然我們傳給子線程看上去是引用傳遞,實際上是将val的值拷貝給了 函數參數 i,可以通過調試程式,檢視各個變量的記憶體位址,就會發現 val 和 val_y記憶體位址相同,但是 i 的位址與val位址不同,這就說明了實際上不是引用傳遞,是值拷貝傳遞。建議使用detach的時候,線程函數,不要寫成引用傳遞。
  • 針對線程函數第二個參數 pmybuf,通過調試檢視位址,發現主線程中的buf位址和線程中的pmybuf記憶體位址相同,如果使用detach,就會産生問題。
  • 是以使用detach的時候不要使用引用傳遞,尤其是不要使用指針(絕對有問題),這會引起錯誤。
  • 解決方式
  1. void myprint(const int i, const string & pmybuf ) {
  2. cout << i << endl;
  3. cout << pmybuf.c_str() << endl;
  4. return;
  5. }
  • 字元數組轉string,隐式類型轉換。
  • 建立臨時對象,最終指派給string,這樣就是不同的記憶體了。

要避免的陷阱(解釋2)

thread mythread(myprint, val, buf); //傳遞參數

  • 代碼執行到這一行,mybuf究竟是什麼時候傳遞給string的?是否main函數都執行完了(此時mybuf被回收了),才把mybuf往string轉。事實上這種方式是有這樣的風險。
  • 更安全的做法(進行顯示類型轉換),将線程的pmybuf綁定到buf轉換成的string臨時對象。

thread mythread(myprint, val, string(buf) ); //傳遞參數

  • 這裡直接将mybuf轉換成string臨時對象,這是一個可以保證線上程中肯定有效的對象。
  •  string(buf)什麼時候轉換?是不是main函數執行完了才開始轉,這樣還是使用了被系統回收的記憶體。
  • 事實上這樣轉沒有問題了。
  • 下面進行證明:
#include <iostream>
#include <string>
#include <thread>
using namespace std;
class A {
public:
int m_i;
//類型轉換構造函數,可以把一個int轉換為類A對象
A(int i) :m_i(i) { cout << "A::A(int i)函數執行了" << endl; }
A(const A &other) :m_i(other.m_i) { cout << "A::A(const A &other)函數執行了" << endl; }
~A() { cout << "A:: ~A()函數執行了" << endl; }
};
void myprint(const int i, const A & p_a ) {
cout << &p_a << endl;  //這裡列印p_a對象的位址
return;
}
int main()
{
int m_val = 1;
int n_val = 22;
thread mythread(myprint, m_val, n_val);  //希望n_val轉換成A類對象傳給myprint第二個參數
mythread.join();
//主線程執行
std::cout << "主線程收尾" << std::endl;
return 0;
}      
  • 上面說明可以通過一個整型構造一個A類型的對象。
  • 如果将上面的join改成detach,則結果如下:
  • 由輸出可知該構造是發生在main函數執行完畢後才開始的。主線程退出後n_val的記憶體空間被回收了,此時還用n_val(無效了)去構造A類對象,這會導緻一些未定義的行為。
  • 我們期望n_val能夠通過A類的類型轉換構造函數構造出對象,但是遺憾的發現直到主線程退出了都沒構造出我們想要的對象然後傳給子線程。
  • 我們使用顯示地進行轉換,構造出臨時對象,然後調用拷貝構造函數将臨時對象拷貝給線程函數的第二個參數p_a.

thread mythread(myprint, m_val, A(n_val));

  • 輸出:
  • 在整個main函數執行完畢之前,肯定已經構造出了臨時對象并且傳遞到線程中去了。
  • 即證明了在建立線程的同時構造臨時對象的方法傳遞參數是可行的。

總結

  • 若傳遞int這種簡單類型參數,建議都是值傳遞,不要用引用,防止節外生枝。
  • 如果傳遞類對象,避免隐式類型轉換。全部都在建立線程這一行就建構出臨時對象來,然後線上程函參數裡,用引用來接(否則系統還會構造臨時對象來接,構造三次)。
  • 建議不使用detach(),隻使用join(),這樣就不存在局部變量失效導緻線程對記憶體的非法引用問題。

臨時對象作為線程參數繼續講

線程id概念

  • id是個數字,每個線程(不管是主線程還是子線程)實際上都對應着一個數字,而且每個線程對應的這個數字都不同。
  • 也就是說,不同的線程,它的線程id(數字)必然是不同。
  • 線程id可以用c++标準庫裡的函數來擷取。通過 std::this_thread::get_id() 來擷取。

臨時對象構造時機抓捕

#include <iostream>
#include <string>
#include <thread>
using namespace std;
class A {
public:
int m_i;
//類型轉換構造函數,可以把一個int轉換為類A對象
A(int i) :m_i(i) { cout << "A::A(int i)函數執行了" << this << "  ThreadId  " \
<< std::this_thread::get_id()<< endl; }
A(const A &other) :m_i(other.m_i) { cout << "A::A(const A &other)函數執行了" << this \
<< "  ThreadId  " << std::this_thread::get_id() << endl; }
~A() { cout << "A:: ~A()函數執行了" << this << "  ThreadId  "  \
<< std::this_thread::get_id() << endl; }
};
void myprint(const A  &p_a ) {
cout << "子線程myprint()參數位址:" << &p_a << "  ThreadId  " \
<< std::this_thread::get_id() << endl;
return;
}
int main()
{
cout << "主線程ID" << std::this_thread::get_id() << endl;
int n_val = 69;
thread mythread(myprint, n_val);  //希望n_val轉換成A類對象傳給myprint第二個參數
mythread.join();
//主線程執行
std::cout << "主線程收尾" << std::endl;
return 0;
}      
  • 注意到n_val構造成A類對象是發生在子線程中的。如果detach就出問題了。

thread mythread(myprint, A(n_val));

  • 使用顯示類型轉換,建立臨時對象的方式,可以主線程執行完畢之前将臨時對象構造出來,然後拷貝到子線程當中去。
  • 如果線程函數中使用值拷貝,不用引用方式:

void myprint(const A p_a )

  • 在子線程中多執行了一次拷貝構造函數,是以建議在類作為參數傳遞時,使用引用方式傳遞(雖然寫的是引用方式,但是實際上是按值拷貝方式處理)。

傳遞類對象、智能指針作為線程參數

線上程中修改變量的值不會影響到主線程。

  • 将類A的成員變量m_i改成mutable。
#include <iostream>
#include <string>
#include <thread>
using namespace std;
class A {
public:
mutable int m_i;
//類型轉換構造函數,可以把一個int轉換為類A對象
A(int i) :m_i(i) { cout << "A::A(int i)函數執行了" << this << "  ThreadId  " \
<< std::this_thread::get_id()<< endl; }
A(const A &other) :m_i(other.m_i) { cout << "A::A(const A &other)函數執行了" << this \
<< "  ThreadId  " << std::this_thread::get_id() << endl; }
~A() { cout << "A:: ~A()函數執行了" << this << "  ThreadId  "  \
<< std::this_thread::get_id() << endl; }
};
void myprint(const A  &p_a ) {
p_a.m_i = 89;
cout << "子線程myprint()參數位址:" << &p_a << "  ThreadId  " \
<< std::this_thread::get_id() << endl;
return;
}
int main()
{
cout << "主線程ID" << std::this_thread::get_id() << endl;
A a(1);
thread mythread(myprint, a);
mythread.join();
//主線程執行
std::cout << "主線程結束" << std::endl;
return 0;
}      
  • 雖然傳進去的是引用,但是線程中對m_i的值進行修改,不會影響到main函數中的a對象的m_i的值。
  • 線程中對象p_a資訊:
  • 線上程中對m_i的發生修改後,此時對象a的資訊:
  • 雖然對象a是以引用傳遞的方式傳給p_a,但是這個過程是拷貝構造的過程,兩個對象的記憶體位址不同。

【std::ref()】

  • 如果需要真正的把對象引用傳遞到線程函數當中,就需要使用 std::ref()
#include <iostream>
#include <string>
#include <thread>
using namespace std;
class A {
public:
int m_i;
//類型轉換構造函數,可以把一個int轉換為類A對象
A(int i) :m_i(i) { cout << "A::A(int i)函數執行了"<< endl; }
A(const A &other) :m_i(other.m_i) { cout << "A::A(const A &other)函數執行了"  << endl; }
~A() { cout << "A:: ~A()函數執行了"  << endl; }
};
void myprint(A  &p_a ) {
p_a.m_i = 89;
cout << "子線程myprint()執行了"  << endl;
return;
}
int main()
{
A a(1);
thread mythread(myprint, std::ref(a));
mythread.join();
//主線程執行
std::cout << "主線程結束" << std::endl;
return 0;
}      
  • 調試時線程中對象p_a資訊:
  • 線上程中對m_i的發生修改後,此時對象a的資訊:
  • 最終輸出:
  • 使用了std::ref() 拷貝構造函數就沒有了,且兩個對象位址相同,實作真正的引用傳遞。

智能指針,想從一個堆棧到另一個堆棧,需要使用std::move()

#include <iostream>
#include <string>
#include <thread>
using namespace std;
void myprint(unique_ptr<int> ptr_u) {
cout << "子線程myprint()執行了"  << endl;
return;
}
int main()
{
unique_ptr<int> m_ptr(new int(100));
thread mythread(myprint, std::move(m_ptr));
mythread.join();
//主線程執行
std::cout << "主線程結束" << std::endl;
return 0;
}      
  • 調試檢視m_ptr資訊:
  • 調試檢視ptr_u資訊:
  • 兩者指向的位址相同。
  • 注意:如果這裡使用detach,就很危險,因為線程中的智能指針指向的是主線程中的一塊記憶體,當主線程執行完畢而子線程中的智能指針還指向這塊記憶體就會出錯。

用成員函數指針做線程函數

#include <iostream>
#include <string>
#include <thread>
using namespace std;
class A {
public:
int m_i;
//類型轉換構造函數,可以把一個int轉換為類A對象
A(int i) :m_i(i) { cout << "A::A(int i)函數執行了"<< endl; }
A(const A &other) :m_i(other.m_i) { cout << "A::A(const A &other)函數執行了"  << endl; }
void func(int i) { cout << "A::func(int i)函數執行了" \
<< "  i =  " << i << endl; }
~A() { cout << "A:: ~A()函數執行了"  << endl; }
};
int main()
{
A a_obj(11);
thread mythread(&A::func, a_obj, 233);
mythread.join();
//主線程執行
std::cout << "主線程結束" << std::endl;
return 0;
}      

【注】類對象使用引用方式傳遞

thread mythread(&A::func, &a_obj, 233);

thread mythread(&A::func, std::ref(a_obj), 233);

  • 使用引用或者std::ref不會調用拷貝構造函數,這時使用detach就要注意了。

【operator()帶參數】

#include <iostream>
#include <string>
#include <thread>
using namespace std;
class A {
public:
int m_i;
//類型轉換構造函數,可以把一個int轉換為類A對象
A(int i) :m_i(i) { cout << "A::A(int i)函數執行了"<< endl; }
A(const A &other) :m_i(other.m_i) { cout << "A::A(const A &other)函數執行了"  << endl; }
void operator()(int i) { cout << "A::operator()執行了" \
<< "  i =  " << i << endl; }
~A() { cout << "A:: ~A()函數執行了"  << endl; }
};
int main()
{
A a_obj(11);
thread mythread(a_obj, 666);
mythread.join();
//主線程執行
std::cout << "主線程結束" << std::endl;
return 0;
}      
  • 改用std::ref() 傳遞可調用對象

thread mythread(std::ref(a_obj), 999);

  • 少了拷貝構造函數進行資源複制,使用detach要小心。
  • 改用 引用& 傳遞可調用對象
  • 這種方式不可以,程式報錯。

使用detach注意事項小結

  • 驗證傳入的參數(類對象)究竟是在主線程中構造完成後傳進去的,還是在子線程中構造建立的。使用線程id 加類的構造函數與拷貝構造函數進行測試。
  • 注意是不是使用了std::ref()進行傳參。
  • 關注是不是主線程中的資源值拷貝方式給了子線程。