多執行緒03:?執行緒傳參詳解

booker發表於2022-05-11

?執行緒傳參詳解

一、傳遞臨時物件作為執行緒引數

1.1要避免的陷阱

直接看程式碼:

#include <iostream>
#include <thread>
using namespace std;

void myPrint(const int &i, char* pmybuf)
{
	//如果執行緒從主執行緒detach了
	// ####1.i不是mvar真正的引用,實際上還是值傳遞(通過檢視地址瞭解到),即使主執行緒執行完畢了,子執行緒用i仍然是安全的,但仍不推薦傳遞引用 !!!!
    //實際上,使用join傳入的也是假引用!
	// 推薦改為const int i !!!
	cout << i << endl;
	//而指標pmybuf還是指向原來的字串,所以這麼寫是不安全的
	cout << pmybuf << endl;
}

int main()
{
	int mvar = 1;
	int& mvary = mvar;
	char mybuf[] = "this is a test";
	thread myThread(myPrint, mvar, mybuf);//第一個引數是函式名,後兩個引數是要傳入函式的引數
	myThread.join();
	//myThread.detach();
	
	cout << "Hello World!" << endl;
    return 0;
}

1.2要避免的陷阱

看程式碼:

#include <iostream>
#include <thread>
#include <string>
using namespace std;

void myPrint(const int i, const string& pmybuf)
{
	cout << i << endl;
	cout << pmybuf << endl;
}

int main()
{
	int mvar = 1;
	int& mvary = mvar;
	char mybuf[] = "this is a test";
	//如果detach了,這樣仍然是不安全的
	//#####2.因為存在主執行緒執行完了,mybuf被回收了,系統才進行mybuf隱式型別轉換成string!!!
	//推薦先建立一個臨時物件thread myThread(myPrint, mvar, string(mybuf));就絕對安全了,這樣就會在主執行緒中完成string型別構造和引數的拷貝構造!!!(可通過自己構造類,加入執行緒中來測試一下);
	thread myThread(myPrint, mvar, mybuf);
	myThread.join();
	//myThread.detach();

	cout << "Hello World!" << endl;
    return 0;
}

1.3總結

  • 如果傳遞int這種簡單型別,推薦使用值傳遞,不要用引用
  • 如果傳遞類物件,避免使用隱式型別轉換,全部都是建立執行緒這一行就建立出臨時物件,然後在函式引數裡,用引用來接,否則還會建立出一個物件
  • 終極結論:建議不使用detach

二、臨時物件作為執行緒引數繼續講

2.1 執行緒id概念

  • id是個數字,每個執行緒(不管是主執行緒還是子執行緒)實際上都對應著一個數字,而且每個執行緒對應的這個數字都不一樣
  • 執行緒id可以用C++標準庫裡的函式來獲取。std::this_thread::get_id()來獲取

2.2 臨時物件構造時機分析

為什麼需要在建立執行緒的時候就建立臨時物件?不能隱式型別轉換呢?

①下面我們來看看隱式型別轉換:

#include <iostream>

using namespace std;
#include<thread>

class A
{
public:
	int _ma;
	A(int ma) : _ma(ma) { cout << "建構函式A()" << " 物件地址:" << this << " 執行緒PID:" << std::this_thread::get_id() << endl; }
	~A() { cout << "解構函式~A()" << " 物件地址:" << this << " 執行緒PID:" << std::this_thread::get_id() << endl; }
	A(const A& a) : _ma(a._ma) { cout << "拷貝建構函式A(const A &a)" << " 物件地址:" << this << " 執行緒PID:" << std::this_thread::get_id() << endl; }
};

void myprint2(const A& a)
{
	cout << "物件地址:" << &a << " 子執行緒PID:" << std::this_thread::get_id() << endl;
}

int main()
{
	cout << "執行緒PID:" << std::this_thread::get_id() << endl;
	int temp = 1;
	thread mytobj(myprint2, temp);//隱式型別轉換
	
	//主執行緒等子執行緒
	mytobj.join();

	//主執行緒子執行緒分離
	//mytobj.detach();
	return 0;
}

結果:

image-20220408213627472

可以看出:存在致命問題:竟然是在子執行緒中創造A類物件,子執行緒函式中的引用起效果。這說明:如果我們採用detach方式分離了主執行緒和子執行緒,一旦主執行緒比子執行緒引數隱式轉換前結束了,那麼子執行緒將會產生不可預估的結果。

②我們在來看看建立臨時物件的方式:

修改上面的thread mytobj(myprint2, temp);,改為:thread mytobj(myprint2, A(temp));

用join和detach方式,都得結果:

image-20220408224558374

可以看出:所有A類物件都在主函式中構造,子執行緒函式中的引用不起效果。 這說明:在採用detach分離了主執行緒和子執行緒,需要傳入臨時物件。

還有一個問題:如果子執行緒函式中不用&,會怎麼樣呢?

image-20220408225417849

會比加引用多了一次子執行緒內的拷貝構造!浪費記憶體,為什麼呢?因為不同的編譯器或平臺,對於thread函式處理的方式是不一樣,可以用這套程式碼區Linux看看。(物件優化問題)

三、傳遞類物件、智慧指標作為執行緒引數

3.1傳遞類物件作為執行緒引數

要基於上面的臨時物件作為執行緒引數,子執行緒裡面改物件的值會改變主執行緒物件的值嗎?繼續寫程式碼:

#include <iostream>
#include <thread>
using namespace std;

class A {
public:
	mutable int m_i; //記住!!!:帶有mutable關鍵字,m_i即使實在const中也可以被修改
	A(int i) :m_i(i) {}
};

void myPrint(const A& pmybuf)
{
	pmybuf.m_i = 199;
	cout << "子執行緒myPrint的引數地址是" << &pmybuf << "thread = " << std::this_thread::get_id() << endl;
}

int main()
{
	A myObj(10);
	//myPrint(const A& pmybuf)中引用不能去掉,如果去掉會多建立一個物件
	//const也不能去掉,去掉會出錯,可能出現無法接受右值情況(傳入臨時物件)
    
	//即使是傳遞的const引用,但在子執行緒中還是會呼叫拷貝建構函式構造一個新的物件,
	//因為主執行緒和子執行緒中物件不同,所以在子執行緒中修改m_i的值不會影響到主執行緒
	thread myThread(myPrint, myObj);
	myThread.join();
	//myThread.detach();

	cout << "Hello World!" << endl;
    
}

//如果希望子執行緒中修改m_i的值影響到主執行緒,可以用thread myThread(myPrint, std::ref(myObj));這樣const就是真的引用了,myPrint定義中的const就可以去掉了,類A定義中的mutable也可以去掉了,因為傳入的就是主執行緒的物件

3.2智慧指標作為執行緒引數

#include <iostream>
#include <thread>
#include <memory>
using namespace std;

void myPrint(unique_ptr<int> ptn)
{
	cout << "thread = " << std::this_thread::get_id() << endl;
}

int main()
{
	unique_ptr<int> up(new int(10));
	//獨佔式指標只能通過std::move()才可以傳遞給另一個指標
	//傳遞後up就指向空,新的ptn指向原來的記憶體
	//所以這時就不能用detach了,因為如果用detach,主執行緒先執行完,new int(10)屬於主執行緒中的洩漏資源,ptn指向的物件就被釋放了!
	thread myThread(myPrint, std::move(up));
	myThread.join();
	//myThread.detach();

	return 0;
}
//這塊忘記了可以去看看智慧指標;

四、用成員函式指標做執行緒函式

#include <iostream>

using namespace std;
#include<thread>

class A
{
public:
	int _ma;
	A(int ma) : _ma(ma) { cout << "建構函式A()" << " 物件地址:" << this << " 執行緒PID:" << std::this_thread::get_id() << endl; }
	~A() { cout << "解構函式~A()" << " 物件地址:" << this << " 執行緒PID:" << std::this_thread::get_id() << endl; }
	A(const A& a) : _ma(a._ma) { cout << "拷貝建構函式A(const A &a)" << " 物件地址:" << this << " 執行緒PID:" << std::this_thread::get_id() << endl; }
	
	void thread_work(int num) // 子執行緒任務
	{
		cout << "子執行緒執行" << " 子執行緒PID:" << std::this_thread::get_id() << endl;
	}
};

int main()
{
	cout << "主執行緒PID:" << std::this_thread::get_id() << endl;
	A myobj(10);
	thread mytobj(&A::thread_work, myobj, 15); //傳入 成員函式 + 類物件 + 成員函式的引數值;
	
	//主執行緒等子執行緒
	mytobj.join();

	//主執行緒子執行緒分離
	//mytobj.detach();
	return 0;
}

join、detach都沒有問題:

image-20220408233057606

可以看出:會在主執行緒中拷貝構造一個臨時物件傳給子執行緒,由子執行緒掌控該物件析構釋放

相關文章