C++並發與多線程學習筆記--多線程數據共享問題


  • 創建和等待多個線程
  • 數據和共享問題分析
    • 只讀的數據
    • 有讀有寫
    • 其他案例
  • 共享數據的保護案例代碼

創建和等待多個線程

服務端后台開發就需要多個線程執行不同的任務。不同的線程執行不同任務,並返回執行結果。很多個線程都用同一個線程入口:

void myprint(int num)
{
	cout << "線程開始執行了: " << num << endl;
	cout << "My print id: "<<this_thread::get_id() << endl;
	cout << "線程結束執行了" << endl;
	return;
}


int main()
{
	vector<thread> workers;
	//創建10個線程,線程的入口統一使用 void myprint(int num)
	for (int i = 0; i < 10; i++) {
		workers.push_back(thread(myprint, i)); //創建並開始執行線程
	}
	for (auto iter = workers.begin(); iter != workers.end(); ++iter) {
		iter->join();
	}

	cout << "Main Thread End!!!" << endl;
	
	return 0;
}

 小結:

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

2)主線程等待所有子線程運行結束,最后主線程才結束。

3)用join寫出來的程序才比較穩定,更容易寫出穩定的程序。

4)專門用迭代器創建多個線程的寫法,創建大量的線程進行管理,很方便。==>線程池的思路。

數據和共享問題分析

只讀的數據

    共享數據

vector<int> g_v = { 1,2,3 }; //共享數據

 修改打印函數

void myprint(int num)
{

cout << "線程的ID為 " << this_thread::get_id() << 
"打印 g_v的值為" << g_v[0] << g_v[1] << g_v[2] << endl; return; }

 雖然順序不定,但是每次都成功讀出了數組中的值。==>只讀數據是安全穩定的,直接可以讀。

有讀有寫

有讀有些的線程,一旦代碼寫不好的時候,容易出問題,崩潰or報錯。

兩個線程往容器里面寫,八個線程往容器里讀,如果沒有特別的處理,程序肯定崩潰。最簡單的不崩潰處理,讀的時候不能寫,寫的時候不能讀。==>互斥鎖的思路??

1)任務切換有各種詭異的事情發生,如程序崩潰。

std::mutex locker;

  

void myprint(int num)
{
	locker.lock();
	cout << "線程的ID為 " << this_thread::get_id() <<
		"打印 g_v的值為" << g_v[0] << g_v[1] << g_v[2] << endl;
	locker.unlock();
	return;
}

  

其他案例

例1:假設定火車票,10個售票窗口。

1,2窗口同時賣票,如果座位已經有人訂了,那么直接返回,告訴顧客已經有人坐了,否則訂票。

1號窗口和2號窗口共享這些數據。

共享數據的保護案例代碼

 實際工作中的范例:網絡游戲服務器開發,網絡游戲服務器,這個服務器,最簡單的有兩個自己創建的線程:(實際中可以用線程池來做):

1) 用來收玩家的命令並把命令數據寫到一個隊列中,這個線程專門負責通過網絡收數據。

2) 線程重隊列中取出玩家發送的命令,解析,執行玩家的動作---抽卡!!!

3) 用數字表示玩家的動作

4) vector, list, list和vector的內部實現手段不一樣,在底層雖然都是push_back(),但是在插入元素的時候,vector需要復制內存到新的空間,並且有[]操作符的重載,而list是數據結構中的雙向鏈表,因此沒有[],在頻繁插入和刪除的時候使用List,容器對於隨機的插入和刪除效率高。

 使用List的時候

#include<list>

 用成員函數作為線程函數的方法寫線程。實際中,一般都把變量寫在類中,符合面向對象的程序設計思想。

class ProcessRequest {
public:
	//把命令加入到一個隊列
	void inMsgRecvQueue() {
		for (int i = 0; i < 100000; ++i) {
			cout << "插入一個元素" << endl;
			m_msgRecvQueue.push_back(i); //假設這個隊列表示玩家的命令
		
		} //占用時間片
	}
	//把命令移出一個隊列
		void outMsgRecvQueue() {

			for (int i = 0; i < 100000; ++i) {
				if (!m_msgRecvQueue.empty()) {
					//消息不為空
					int command = m_msgRecvQueue.front();
					//嘗試返回第一個元素,取出元素
					m_msgRecvQueue.pop_front();
				}
				else
				{
					cout << "outMsgRecvQueue() 還執行,但是消息隊列為空"<<i<< endl;
					//消息隊列為空
				}
				
			} //占用時間片
			cout << "end!!!!!" << endl;
		}
	
private:
	std::list<int> m_msgRecvQueue; //容器,用於表示玩家的發送過來命令

};

 主函數

	ProcessRequest obj;
	std::thread outWorker(&ProcessRequest::outMsgRecvQueue, &obj);
	//第二參數是引用才是同一個對象,不能用detach(),否則不穩定
	std::thread inWorker(&ProcessRequest::inMsgRecvQueue, &obj);
	//兩個線程創建完成之后,要保證對象是有意義的,用join
	outWorker.join();
	inWorker.join();

 數據共享的理論,有讀有寫,不斷地讀和寫,共享的消息隊列,如果完全不控制,一定會出錯。共享數據與鎖,某個線程在操作的時候,其他線程需要等待。寫的時候鎖住,讀的時候鎖住。

1)互斥量。多線程一定會有互斥量,下回分解。。。

其他知識

vector和built-in數組類似,它擁有一段連續的內存空間,並且起始地址不變,因此它能非常好的支持隨即存取,即[]操作符,但由於它的內存空間是連續的,所以在中間進行插入和刪除會造成內存塊的拷貝,另外,當該數組后的內存空間不夠時,需要重新申請一塊足夠大的內存並進行內存的拷貝。這些都大大影響了vector的效率。

list就是數據結構中的雙向鏈表(根據sgi stl源代碼),因此它的內存空間可以是不連續的,通過指針來進行數據的訪問,這個特點使得它的隨即存取變的非常沒有效率,因此它沒有提供[]操作符的重載。但由於鏈表的特點,它可以以很好的效率支持任意地方的刪除和插入。

deque是一個double-ended queue,它的具體實現不太清楚,但知道它具有以下兩個特點:
它支持[]操作符,也就是支持隨即存取,並且和vector的效率相差無幾,它支持在兩端的操作:push_back,push_front,pop_back,pop_front等,並且在兩端操作上與list的效率也差不多。

因此在實際使用時,如何選擇這三個容器中哪一個,應根據你的需要而定,一般應遵循下面
的原則:
  1、如果你需要高效的隨即存取,而不在乎插入和刪除的效率,使用vector
  2、如果你需要大量的插入和刪除,而不關心隨即存取,則應使用list
  3、如果你需要隨即存取,而且關心兩端數據的插入和刪除,則應使用deque。

 

 

參考文獻

https://study.163.com/course/courseLearn.htm?courseId=1006067356#/learn/video?lessonId=1053470368&courseId=1006067356

https://blog.csdn.net/bmjhappy/article/details/82228080


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM