[c++11]多線程編程(三)——競(jìng)爭(zhēng)條件與互斥鎖

競(jìng)爭(zhēng)條件

并發(fā)代碼中最常見的錯(cuò)誤之一就是競(jìng)爭(zhēng)條件(race condition)育特。而其中最常見的就是數(shù)據(jù)競(jìng)爭(zhēng)(data race)丙号,從整體上來看,所有線程之間共享數(shù)據(jù)的問題缰冤,都是修改數(shù)據(jù)導(dǎo)致的犬缨,如果所有的共享數(shù)據(jù)都是只讀的,就不會(huì)發(fā)生問題棉浸。但是這是不可能的怀薛,大部分共享數(shù)據(jù)都是要被修改的。

c++中常見的cout就是一個(gè)共享資源迷郑,如果在多個(gè)線程同時(shí)執(zhí)行cout枝恋,你會(huì)發(fā)發(fā)現(xiàn)很奇怪的問題:

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

// 普通函數(shù) 無參
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;
}

你有很大的幾率發(fā)現(xiàn)打印會(huì)出現(xiàn)類似于From t1: From main: 64這樣奇怪的打印結(jié)果。cout是基于流的嗡害,會(huì)先將你要打印的內(nèi)容放入緩沖區(qū)焚碌,可能剛剛一個(gè)線程剛剛放入From t1:,另一個(gè)線程就執(zhí)行了霸妹,導(dǎo)致輸出變亂十电。而c語言中的printf不會(huì)發(fā)生這個(gè)問題。

使用互斥元保護(hù)共享數(shù)據(jù)

解決辦法就是要對(duì)cout這個(gè)共享資源進(jìn)行保護(hù)抑堡。在c++中摆出,可以使用互斥鎖std::mutex進(jìn)行資源保護(hù)朗徊,頭文件是#include <mutex>首妖,共有兩種操作:鎖定(lock)解鎖(unlock)。將cout重新封裝成一個(gè)線程安全的函數(shù):

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

std::mutex mu;
// 使用鎖保護(hù)
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;
}

修改完之后爷恳,運(yùn)行可以發(fā)現(xiàn)打印沒有問題了有缆。但是還有一個(gè)隱藏著的問題,如果mu.lock()mu.unlock()之間的語句發(fā)生了異常,會(huì)發(fā)生什么棚壁?unlock()語句沒有機(jī)會(huì)執(zhí)行杯矩!導(dǎo)致導(dǎo)致mu一直處于鎖著的狀態(tài),其他使用shared_print()函數(shù)的線程就會(huì)阻塞袖外。

解決這個(gè)問題也很簡(jiǎn)單史隆,使用c++中常見的RAII技術(shù),即獲取資源即初始化(Resource Acquisition Is Initialization)技術(shù)曼验,這是c++中管理資源的常用方式泌射。簡(jiǎn)單的說就是在類的構(gòu)造函數(shù)中創(chuàng)建資源,在析構(gòu)函數(shù)中釋放資源鬓照,因?yàn)榫退惆l(fā)生了異常熔酷,c++也能保證類的析構(gòu)函數(shù)能夠執(zhí)行。我們不需要自己寫個(gè)類包裝mutex豺裆,c++庫已經(jīng)提供了std::lock_guard類模板拒秘,使用方法如下:

void shared_print(string msg, int id) {
    //構(gòu)造的時(shí)候幫忙上鎖,析構(gòu)的時(shí)候釋放鎖
    std::lock_guard<std::mutex> guard(mu);
    //mu.lock(); // 上鎖
    cout << msg << id << endl;
    //mu.unlock(); // 解鎖
}

可以實(shí)現(xiàn)自己的std::lock_guard臭猜,類似這樣:

class MutexLockGuard
{
 public:
  explicit MutexLockGuard(std::mutex& mutex)
    : mutex_(mutex)
  {
    mutex_.lock();
  }

  ~MutexLockGuard()
  {
    mutex_.unlock();
  }

 private:
  std::mutex& mutex_;
};

為保護(hù)共享數(shù)據(jù)精心組織代碼

上面的std::mutex互斥元是個(gè)全局變量躺酒,他是為shared_print()準(zhǔn)備的,這個(gè)時(shí)候获讳,我們最好將他們綁定在一起阴颖,比如說,可以封裝成一個(gè)類丐膝。由于cout是個(gè)全局共享的變量量愧,沒法完全封裝,就算你封裝了帅矗,外面還是能夠使用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類封裝了一個(gè)mutex和一個(gè)ofstream對(duì)象浑此,然后shared_print函數(shù)在mutex的保護(hù)下累颂,是線程安全的。使用的時(shí)候凛俱,先定義一個(gè)LogFile的實(shí)例log紊馏,主線程中直接使用,子線程中通過引用傳遞過去(也可以使用單例來實(shí)現(xiàn)),這樣就能保證資源被互斥鎖保護(hù)著蒲犬,外面沒辦法使用但是使用資源朱监。

但是這個(gè)時(shí)候還是得小心了!用互斥元保護(hù)數(shù)據(jù)并不只是像上面那樣保護(hù)每個(gè)函數(shù)原叮,就能夠完全的保證線程安全赫编,如果將資源的指針或者引用不小心傳遞出來了巡蘸,所有的保護(hù)都白費(fèi)了!要記住一下兩點(diǎn):

  1. 不要提供函數(shù)讓用戶獲取資源擂送。

    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 !!!
        }
    };
    
  2. 不要資源傳遞給用戶的函數(shù)悦荒。

    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);
        }
    };
    

以上兩種做法都會(huì)將資源暴露給用戶,造成不必要的安全隱患嘹吨。

接口設(shè)計(jì)中也存在競(jìng)爭(zhēng)條件

STL中的stack類是線程不安全的搬味,于是你模仿著想寫一個(gè)屬于自己的線程安全的類Stack。于是蟀拷,你在pushpop等操作得時(shí)候身腻,加了互斥鎖保護(hù)數(shù)據(jù)。但是在多線程環(huán)境下使用使用你的Stack類的時(shí)候匹厘,卻仍然有可能是線程不安全的嘀趟,why?

假設(shè)你的Stack類的接口如下:

class Stack
{
public:
    Stack() {}
    void pop(); //彈出棧頂元素
    int& top(); //獲取棧頂元素
    void push(int x);//將元素放入棧
private:
    vector<int> data; 
    std::mutex _mu; //保護(hù)內(nèi)部數(shù)據(jù)
};

類中的每一個(gè)函數(shù)都是線程安全的愈诚,但是組合起來卻不是她按。加入棧中有9,3,8,6共4個(gè)元素,你想使用兩個(gè)線程分別取出棧中的元素進(jìn)行處理炕柔,如下所示:

   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

可以發(fā)現(xiàn)在這種執(zhí)行順序下酌泰, 棧頂元素被處理了兩遍,而且多彈出了一個(gè)元素8匕累,導(dǎo)致`8沒有被處理陵刹!這就是由于接口設(shè)計(jì)不當(dāng)引起的競(jìng)爭(zhēng)。解決辦法就是將這兩個(gè)接口合并為一個(gè)接口欢嘿!就可以得到線程安全的棧衰琐。

class Stack
{
public:
    Stack() {}
    int& pop(); //彈出棧頂元素并返回
    void push(int x);//將元素放入棧
private:
    vector<int> data; 
    std::mutex _mu; //保護(hù)內(nèi)部數(shù)據(jù)
};

//下面這樣使用就不會(huì)發(fā)生問題
int v = st.pop(); // 6
process(v);

但是注意:這樣修改之后是線程安全的,但是并不是異常安全的炼蹦,這也是為什么STL中棧的出棧操作分解成了兩個(gè)步驟的原因羡宙。(為什么不是異常安全的還沒想明白。掐隐。)

所以狗热,為了保護(hù)共享數(shù)據(jù),還得好好設(shè)計(jì)接口才行虑省。

參考

  1. C++并發(fā)編程實(shí)戰(zhàn)
  2. C++ Threading #3: Data Race and Mutex
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末匿刮,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子探颈,更是在濱河造成了極大的恐慌熟丸,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,544評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件膝擂,死亡現(xiàn)場(chǎng)離奇詭異虑啤,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)架馋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門狞山,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人叉寂,你說我怎么就攤上這事萍启。” “怎么了屏鳍?”我有些...
    開封第一講書人閱讀 162,764評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵勘纯,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我钓瞭,道長(zhǎng)驳遵,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,193評(píng)論 1 292
  • 正文 為了忘掉前任山涡,我火速辦了婚禮堤结,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘鸭丛。我一直安慰自己竞穷,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,216評(píng)論 6 388
  • 文/花漫 我一把揭開白布鳞溉。 她就那樣靜靜地躺著瘾带,像睡著了一般。 火紅的嫁衣襯著肌膚如雪熟菲。 梳的紋絲不亂的頭發(fā)上看政,一...
    開封第一講書人閱讀 51,182評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音抄罕,去河邊找鬼帽衙。 笑死,一個(gè)胖子當(dāng)著我的面吹牛贞绵,可吹牛的內(nèi)容都是我干的厉萝。 我是一名探鬼主播,決...
    沈念sama閱讀 40,063評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼榨崩,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼谴垫!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起母蛛,我...
    開封第一講書人閱讀 38,917評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤翩剪,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后彩郊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體前弯,經(jīng)...
    沈念sama閱讀 45,329評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蚪缀,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,543評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了恕出。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片询枚。...
    茶點(diǎn)故事閱讀 39,722評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖浙巫,靈堂內(nèi)的尸體忽然破棺而出金蜀,到底是詐尸還是另有隱情,我是刑警寧澤的畴,帶...
    沈念sama閱讀 35,425評(píng)論 5 343
  • 正文 年R本政府宣布渊抄,位于F島的核電站,受9級(jí)特大地震影響丧裁,放射性物質(zhì)發(fā)生泄漏护桦。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,019評(píng)論 3 326
  • 文/蒙蒙 一煎娇、第九天 我趴在偏房一處隱蔽的房頂上張望嘶炭。 院中可真熱鬧,春花似錦逊桦、人聲如沸眨猎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽睡陪。三九已至,卻和暖如春匿情,著一層夾襖步出監(jiān)牢的瞬間兰迫,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評(píng)論 1 269
  • 我被黑心中介騙來泰國打工炬称, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留汁果,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,729評(píng)論 2 368
  • 正文 我出身青樓玲躯,卻偏偏與公主長(zhǎng)得像据德,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子跷车,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,614評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容