C++併發與多線程 創建多個線程、數據問題共享分析、mutex、lock_guard、死鎖、std::lock()、std::adopt_loc

創建多個線程和等待多個線程

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

void print(int num)
{
	cout << "print執行,線程編號:" << num << endl;
	cout << "print結束,線程編號:" << num << endl;
	return;
}
int main()
{
	vector<thread>v;
	for (int i = 0; i < 10; ++i)
	{
		v.push_back (thread(print, i));//創建10個線程並開始執行線程
	}
	for (auto iter = v.begin(); iter != v.end(); ++iter)
	{
		iter->join();
	}
	cout << "hello!" << endl;
	return 0;

}

在這裏插入圖片描述

可以得出結論:

1.多個線程執行的順序是亂的,跟操作系統內部調度機制有關。

2.主線程等待所有子線程運行結束以後,最後主線程結束,推薦這種寫法,更容易寫出穩定的程序。


數據共享問題分析

1.只讀數據不修改

void print(int num)
{
	cout << "線程id: " << this_thread::get_id() << ", " << vals[0] << vals[1] << vals[2] << endl;
	return;
}
int main()
{
	vector<thread>v;
	for (int i = 0; i < 10; ++i)
	{
		v.push_back (thread(print, i));//創建10個線程並開始執行線程
	}
	for (auto iter = v.begin(); iter != v.end(); ++iter)
	{
		iter->join();
	}
	cout << "hello!" << endl;
	return 0;

}

在這裏插入圖片描述

可以看到雖然順序是不穩定的,但是每次都成功打印了,只讀數據是安全穩定的,不需要什麼處理手段.

實際案例

class A
{
private:
	list<int>msgqueue;
public:
	void MsgEnqueue()
	{
		for (int i = 0; i < 10000; ++i)
		{
			cout << "MsgEnqueue()執行,插入一個元素 " << i << endl;
			msgqueue.push_back(i);
		}
	}
	void MsgDequeue()
	{
		for (int i = 0; i < 10000; ++i)
		{
			if (!msgqueue.empty())
			{
				int command = msgqueue.front();
				msgqueue.pop_front();
				
			}
			else
			{
				cout << "MsgEnqueue()執行,但隊列爲空" << endl;
			}

		}
	}
};
int main()
{
	A a;
	thread t1(&A::MsgEnqueue, &a);
	thread t2(&A::MsgDequeue, &a);
	t1.join();
	t2.join();
	cout << endl;

}

在這裏插入圖片描述

可以看到由於沒有保護,一個線程瘋狂入隊,另一個線程瘋狂出隊。程序運行時很快就崩潰了。

解決辦法:保護共享數據,操作時,操作時,某個線程用代碼把共享數據鎖住,其他想操作共享數據的線程必須等待解鎖。

互斥量

互斥量是一個類對象,理解成一把鎖,多個線程嘗試使用Lock()成員函數來加鎖這把鎖頭,只有一個線程能鎖定成功,如果沒鎖成功,流程會卡在lock()這裏不斷的嘗試去加鎖。互斥量使用要小心,保護數據少了,達不到保護的效果,多了影響效率

頭文件<mutex>

lock()和unlock()

使用方法:

#include <iostream>
#include <thread>
#include <mutex>
#include <list>
using namespace std;
class A
{
private:
	list<int>msgqueue;
	mutex mymutex;
public:
	void MsgEnqueue()
	{
		for (int i = 0; i < 10000; ++i)
		{

			cout << "MsgEnqueue()執行,插入一個元素 " << i << endl;
			mymutex.lock();
			msgqueue.push_back(i);
			mymutex.unlock();
		}
		return;
	}
	bool helper(int & command)
	{
		mymutex.lock();
		if (!msgqueue.empty())
		{
			command = msgqueue.front();
			msgqueue.pop_front();
			mymutex.unlock();
			return true;
		}
		mymutex.unlock();
		return false;
	}
	void MsgDequeue()
	{
		int command = 0;
		for (int i = 0; i < 10000; ++i)
		{
			bool result = helper(command);
			if (result)
			{
				cout << "helper執行,取出一個元素 " << command << endl;
			}
			else
			{
				cout << "MsgEnqueue()執行,但隊列爲空 " <<  i <<endl;
			}

		}
	}
};
int main()
{
	A a;
	thread t1(&A::MsgEnqueue, &a);
	thread t2(&A::MsgDequeue, &a);
	t1.join();
	t2.join();
	cout << endl;

}

在這裏插入圖片描述

這個時候運行就不會出錯了。

使用總結:
1.先lock(),在unlock().
2. lock()和unlock()要成對執行,如果是有多個分支比如if else這種,每個分支都要unlock,因爲是多個出口.

lock_guard使用方法

爲了防止忘記unlock,提供了lock_guard更方便的使用方法

工作原理:類似智能指針,構造的時候調用鎖的lock()函數,析構的時候調用unlock()函數,作用域結束的時候也就自動銷燬調用unlock().

#include <iostream>
#include <thread>
#include <mutex>
#include <list>
using namespace std;
class A
{
private:
	list<int>msgqueue;
	mutex mymutex;
public:
	void MsgEnqueue()
	{

		for (int i = 0; i < 10000; ++i)
		{

			cout << "MsgEnqueue()執行,插入一個元素 " << i << endl;
			mymutex.lock();
			msgqueue.push_back(i);
			mymutex.unlock();
		}
		return;
	}
	bool helper(int & command)
	{
		lock_guard<mutex>a(mymutex);
		if (!msgqueue.empty())
		{
			command = msgqueue.front();
			msgqueue.pop_front();
			//mymutex.unlock();
			return true;
		}
		//mymutex.unlock();
		return false;
	}
	void MsgDequeue()
	{
		int command = 0;
		for (int i = 0; i < 10000; ++i)
		{
			bool result = helper(command);
			if (result)
			{
				cout << "helper執行,取出一個元素 " << command << endl;
			}
			else
			{
				cout << "MsgEnqueue()執行,但隊列爲空 " <<  i <<endl;
			}

		}
	}
};
int main()
{
	A a;
	thread t1(&A::MsgEnqueue, &a);
	thread t2(&A::MsgDequeue, &a);
	t1.join();
	t2.join();
	cout << endl;

}

可以加個花括號,縮小作用域從而提前析構。

好處:使用簡單,不怕忘記unlock()。

缺點:不夠靈活,不能隨時unlock(),只有析構的時候才能解鎖,不能更精確的控制加鎖和解鎖。



死鎖

兩個或兩個以上鎖(互斥量)會有可能造成死鎖問題

產生的原因,舉個例子:

假設有兩把鎖1和鎖2,有兩個線程A和B:
1.線程A執行,先加鎖鎖1,鎖1lock()成功,正打算lock()鎖2.
然後上下文切換
2.線程B執行,先加鎖鎖2,鎖2lock()成功,正打算lock()鎖1.
此時此刻,死鎖就產生了。
線程A因爲加鎖不了鎖2,流程走不下去。
線程B因爲加鎖不了鎖1,流程也走不下去。
就這樣僵持住了,導致死鎖。

死鎖演示:

#include <iostream>
#include <thread>
#include <mutex>
#include <list>
using namespace std;
class A
{
private:
	list<int>msgqueue;
	mutex mymutex1;
	mutex mymutex2;
public:
	void MsgEnqueue()
	{

		for (int i = 0; i < 10000; ++i)
		{

			cout << "MsgEnqueue()執行,插入一個元素 " << i << endl;
			mymutex1.lock();
			mymutex2.lock();
			msgqueue.push_back(i);
			mymutex2.unlock();
			mymutex1.unlock();
		}
		return;
	}
	bool helper(int & command)
	{
		mymutex2.lock();
		mymutex1.lock();
		if (!msgqueue.empty())
		{
			command = msgqueue.front();
			msgqueue.pop_front();
			mymutex2.unlock();
			mymutex1.unlock();
			return true;
		}
		mymutex2.unlock();
		mymutex1.unlock();
		return false;
	}
	void MsgDequeue()
	{
		int command = 0;
		for (int i = 0; i < 10000; ++i)
		{
			bool result = helper(command);
			if (result)
			{
				cout << "helper執行,取出一個元素 " << command << endl;
			}
			else
			{
				cout << "MsgEnqueue()執行,但隊列爲空 " <<  i <<endl;
			}

		}
	}
};
int main()
{
	A a;
	thread t1(&A::MsgEnqueue, &a);
	thread t2(&A::MsgDequeue, &a);
	t1.join();
	t2.join();
	cout << endl;

}

在這裏插入圖片描述
可以明確看到程序卡住不動了,死鎖了。

解決死鎖的辦法:保證相同的上鎖順序,比如線程A先lock鎖1在lock鎖2,那線程B也應該先lock鎖1在lock鎖2.

std::lock()函數模板

能力:一次鎖住兩個或兩個以上的互斥量(至少兩個,多了也不行),不存在在多線程中,因爲鎖的順序而造成死鎖的風險。如果互斥量中有一個沒鎖住,就等待所以互斥量鎖住,要麼多個鎖都鎖住了,要麼都不鎖,如果其他中一個鎖鎖住了,另外一個上鎖失敗,那就會把其他的鎖也解鎖。

使用例子:

class A
{
private:
	list<int>msgqueue;
	mutex mymutex1;
	mutex mymutex2;
public:
	void MsgEnqueue()
	{

		for (int i = 0; i < 10000; ++i)
		{

			cout << "MsgEnqueue()執行,插入一個元素 " << i << endl;
			lock(mymutex1, mymutex2);
			msgqueue.push_back(i);
			mymutex2.unlock();
			mymutex1.unlock();
		}
		return;
	}
	bool helper(int & command)
	{
		lock(mymutex1, mymutex2);
		if (!msgqueue.empty())
		{
			command = msgqueue.front();
			msgqueue.pop_front();
			mymutex2.unlock();
			mymutex1.unlock();
			return true;
		}
		mymutex2.unlock();
		mymutex1.unlock();
		return false;
	}
	void MsgDequeue()
	{
		int command = 0;
		for (int i = 0; i < 10000; ++i)
		{
			bool result = helper(command);
			if (result)
			{
				cout << "helper執行,取出一個元素 " << command << endl;
			}
			else
			{
				cout << "MsgEnqueue()執行,但隊列爲空 " <<  i <<endl;
			}

		}
	}
};
int main()
{
	A a;
	thread t1(&A::MsgEnqueue, &a);
	thread t2(&A::MsgDequeue, &a);
	t1.join();
	t2.join();
	cout << endl;
}

在這裏插入圖片描述
缺點:還是需要手動unlock很有可能會忘記。
解決辦法:使用lock_guard配合std::adopt_lock,自動調用unlock
使用例子:

#include <iostream>
#include <thread>
#include <mutex>
#include <list>
using namespace std;
class A
{
private:
	list<int>msgqueue;
	mutex mymutex1;
	mutex mymutex2;
public:
	void MsgEnqueue()
	{

		for (int i = 0; i < 10000; ++i)
		{

			cout << "MsgEnqueue()執行,插入一個元素 " << i << endl;
			lock(mymutex1, mymutex2);
			lock_guard<mutex>(mymutex1, std::adopt_lock);
			lock_guard<mutex>(mymutex2, std::adopt_lock);
			msgqueue.push_back(i);
		}
		return;
	}
	bool helper(int & command)
	{
		lock(mymutex1, mymutex2);
		lock_guard<mutex>(mymutex1, std::adopt_lock);
		lock_guard<mutex>(mymutex2, std::adopt_lock);
		if (!msgqueue.empty())
		{
			command = msgqueue.front();
			msgqueue.pop_front();
			return true;
		}
		return false;
	}
	void MsgDequeue()
	{
		int command = 0;
		for (int i = 0; i < 10000; ++i)
		{
			bool result = helper(command);
			if (result)
			{
				cout << "helper執行,取出一個元素 " << command << endl;
			}
			else
			{
				cout << "MsgEnqueue()執行,但隊列爲空 " <<  i <<endl;
			}

		}
	}
};
int main()
{
	A a;
	thread t1(&A::MsgEnqueue, &a);
	thread t2(&A::MsgDequeue, &a);
	t1.join();
	t2.join();
	cout << endl;
}

std::adopt_loc是一個結構體對象,起到標記的作用,有了這個標記lock_guard就不會調用lock函數了

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章