併發代碼中最多見的錯誤之一就是競爭條件(race condition)。而其中最多見的就是數據競爭(data race),從總體上來看,全部線程之間共享數據的問題,都是修改數據致使的,若是全部的共享數據都是隻讀的,就不會發生問題。可是這是不可能的,大部分共享數據都是要被修改的。ios
而c++
中常見的cout
就是一個共享資源,若是在多個線程同時執行cout
,你會發發現很奇怪的問題:c++
#include <iostream> #include <thread> #include <string> using namespace std; // 普通函數 無參 void function_1() { for(int i=0; i>-100; i--) cout << "From t1: " << i << endl; } int main() { std::thread t1(function_1); for(int i=0; i<100; i++) cout << "From main: " << i << endl; t1.join(); return 0; }
你有很大的概率發現打印會出現相似於From t1: From main: 64
這樣奇怪的打印結果。cout
是基於流的,會先將你要打印的內容放入緩衝區,可能剛剛一個線程剛剛放入From t1:
,另外一個線程就執行了,致使輸出變亂。而c
語言中的printf
不會發生這個問題。編程
解決辦法就是要對cout
這個共享資源進行保護。在c++
中,可使用互斥鎖std::mutex
進行資源保護,頭文件是#include <mutex>
,共有兩種操做:鎖定(lock)與解鎖(unlock)。將cout
從新封裝成一個線程安全的函數:安全
#include <iostream> #include <thread> #include <string> #include <mutex> using namespace std; std::mutex mu; // 使用鎖保護 void shared_print(string msg, int id) { mu.lock(); // 上鎖 cout << msg << id << endl; mu.unlock(); // 解鎖 } void function_1() { for(int i=0; i>-100; i--) shared_print(string("From t1: "), i); } int main() { std::thread t1(function_1); for(int i=0; i<100; i++) shared_print(string("From main: "), i); t1.join(); return 0; }
修改完以後,運行能夠發現打印沒有問題了。可是還有一個隱藏着的問題,若是mu.lock()
和mu.unlock()
之間的語句發生了異常,會發生什麼?unlock()
語句沒有機會執行!致使致使mu
一直處於鎖着的狀態,其餘使用shared_print()
函數的線程就會阻塞。多線程
解決這個問題也很簡單,使用c++
中常見的RAII
技術,即獲取資源即初始化(Resource Acquisition Is Initialization)技術,這是c++
中管理資源的經常使用方式。簡單的說就是在類的構造函數中建立資源,在析構函數中釋放資源,由於就算髮生了異常,c++
也能保證類的析構函數可以執行。咱們不須要本身寫個類包裝mutex
,c++
庫已經提供了std::lock_guard
類模板,使用方法以下:併發
void shared_print(string msg, int id) { //構造的時候幫忙上鎖,析構的時候釋放鎖 std::lock_guard<std::mutex> guard(mu); //mu.lock(); // 上鎖 cout << msg << id << endl; //mu.unlock(); // 解鎖 }
能夠實現本身的std::lock_guard
,相似這樣:ide
class MutexLockGuard { public: explicit MutexLockGuard(std::mutex& mutex) : mutex_(mutex) { mutex_.lock(); } ~MutexLockGuard() { mutex_.unlock(); } private: std::mutex& mutex_; };
上面的std::mutex
互斥元是個全局變量,他是爲shared_print()
準備的,這個時候,咱們最好將他們綁定在一塊兒,好比說,能夠封裝成一個類。因爲cout
是個全局共享的變量,無法徹底封裝,就算你封裝了,外面仍是可以使用cout
,而且不用經過鎖。下面使用文件流舉例:函數
#include <iostream> #include <thread> #include <string> #include <mutex> #include <fstream> using namespace std; std::mutex mu; class LogFile { std::mutex m_mutex; ofstream f; public: LogFile() { f.open("log.txt"); } ~LogFile() { f.close(); } void shared_print(string msg, int id) { std::lock_guard<std::mutex> guard(mu); f << msg << id << endl; } }; void function_1(LogFile& log) { for(int i=0; i>-100; i--) log.shared_print(string("From t1: "), i); } int main() { LogFile log; std::thread t1(function_1, std::ref(log)); for(int i=0; i<100; i++) log.shared_print(string("From main: "), i); t1.join(); return 0; }
上面的LogFile
類封裝了一個mutex
和一個ofstream
對象,而後shared_print
函數在mutex
的保護下,是線程安全的。使用的時候,先定義一個LogFile
的實例log
,主線程中直接使用,子線程中經過引用傳遞過去(也可使用單例來實現),這樣就能保證資源被互斥鎖保護着,外面沒辦法使用可是使用資源。ui
可是這個時候仍是得當心了!用互斥元保護數據並不僅是像上面那樣保護每一個函數,就可以徹底的保證線程安全,若是將資源的指針或者引用不當心傳遞出來了,全部的保護都白費了!要記住一下兩點:this
不要提供函數讓用戶獲取資源。
std::mutex mu; class LogFile { std::mutex m_mutex; ofstream f; public: LogFile() { f.open("log.txt"); } ~LogFile() { f.close(); } void shared_print(string msg, int id) { std::lock_guard<std::mutex> guard(mu); f << msg << id << endl; } // Never return f to the outside world ofstream& getStream() { return f; //never do this !!! } };
不要資源傳遞給用戶的函數。
class LogFile { std::mutex m_mutex; ofstream f; public: LogFile() { f.open("log.txt"); } ~LogFile() { f.close(); } void shared_print(string msg, int id) { std::lock_guard<std::mutex> guard(mu); f << msg << id << endl; } // Never return f to the outside world ofstream& getStream() { return f; //never do this !!! } // Never pass f as an argument to user provided function void process(void fun(ostream&)) { fun(f); } };
以上兩種作法都會將資源暴露給用戶,形成沒必要要的安全隱患。
STL
中的stack
類是線程不安全的,因而你模仿着想寫一個屬於本身的線程安全的類Stack
。因而,你在push
和pop
等操做得時候,加了互斥鎖保護數據。可是在多線程環境下使用使用你的Stack
類的時候,卻仍然有多是線程不安全的,why?
假設你的Stack
類的接口以下:
class Stack { public: Stack() {} void pop(); //彈出棧頂元素 int& top(); //獲取棧頂元素 void push(int x);//將元素放入棧 private: vector<int> data; std::mutex _mu; //保護內部數據 };
類中的每個函數都是線程安全的,可是組合起來卻不是。加入棧中有9,3,8,6
共4個元素,你想使用兩個線程分別取出棧中的元素進行處理,以下所示:
Thread A Thread B int v = st.top(); // 6 int v = st.top(); // 6 st.pop(); //彈出6 st.pop(); //彈出8 process(v);//處理6 process(v); //處理6
能夠發如今這種執行順序下, 棧頂元素被處理了兩遍,並且多彈出了一個元素8
,致使`8沒有被處理!這就是因爲接口設計不當引發的競爭。解決辦法就是將這兩個接口合併爲一個接口!就能夠獲得線程安全的棧。
class Stack { public: Stack() {} int& pop(); //彈出棧頂元素並返回 void push(int x);//將元素放入棧 private: vector<int> data; std::mutex _mu; //保護內部數據 }; //下面這樣使用就不會發生問題 int v = st.pop(); // 6 process(v);
可是注意:這樣修改以後是線程安全的,可是並非異常安全的,這也是爲何STL
中棧的出棧操做分解成了兩個步驟的緣由。(爲何不是異常安全的還沒想明白。。)
因此,爲了保護共享數據,還得好好設計接口才行。