- 創建和等待多個線程
- 數據和共享問題分析
- 只讀的數據
- 有讀有寫
- 其他案例
- 共享數據的保護案例代碼
創建和等待多個線程
服務端后台開發就需要多個線程執行不同的任務。不同的線程執行不同任務,並返回執行結果。很多個線程都用同一個線程入口:
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。
參考文獻