欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

C++多線程中互斥量的使用詳解

 更新時間:2023年08月05日 08:34:57   作者:weihao-ysgs  
這篇文章主要介紹了C++多線程中互斥量的使用,本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下

多線程中互斥信號量(Mutex)的使用

1.0 互斥量的基本概念

1.1 Example

\(\quad\)首先我們要明白,為什么會有互斥信號量的出現(xiàn),在多線程編程中,不同的線程之間往往要對同一個數(shù)據(jù)進行操作,如果該數(shù)據(jù)是只讀的,當(dāng)然不會出現(xiàn)什么問題,但是如果兩個線程同時對某個數(shù)據(jù)進行寫操作,則可能出現(xiàn)難以預(yù)料的事情。

  • 我們來看一個簡單的操作
#include <atomic>
#include <iostream>
#include <thread>
#include <chrono>
#include <pthread.h>
using namespace std;
int i = 0;
const int maxCnt = 1000000;
void mythread()
{
  for (int j = 0; j < maxCnt; j++)
  {
    i++;  // 線程同時操作變量
  }
}
int main()
{
  auto begin = chrono::high_resolution_clock::now();
  thread t1(mythread);
  thread t2(mythread);
  t1.join();
  t2.join();
  auto end = chrono::high_resolution_clock::now();
  cout << "i=" << i << endl;
  cout << "time: "
       << chrono::duration_cast<chrono::microseconds>(end - begin).count() *
              1e-6
       << "s" << endl;  // 秒計時
}

可以看到在我的電腦上程序的輸出為

i=1022418
time: 0.010445s

很明顯和我們預(yù)想的結(jié)果是不一致的,我們使用兩個線程同時對該變量進行加法操作,根據(jù)運行此書來計算,結(jié)果因該為 2000000,但事實上卻不是這樣的,這就是因為有多個線程在對同一個變量進行寫操作的時候會出現(xiàn)難以排查的問題,意想不到的結(jié)果。此時mutex就派上用場了,我們對程序進行稍微的改動。

std::mutex var_mutex;
int i = 0;
const int maxCnt = 1000000;
void mythread()
{
  for (int j = 0; j < maxCnt; j++)
  {
    var_mutex.lock();
    i++;  // 線程同時操作變量
    var_mutex.unlock();
  }
}

此時再運行程序可以發(fā)現(xiàn)結(jié)果如下,這是符合我們的預(yù)期的。

i=2000000
time: 0.09337s

1.2 互斥量用法解釋

\(\quad\)互斥量就是個類對象,可以理解成一把鎖,多個線程嘗試用lock()成員函數(shù)來加鎖,從而獲得對數(shù)據(jù)的訪問權(quán)限,或者說讀寫權(quán)限,其實就是繼續(xù)執(zhí)行代碼的權(quán)限。最終只有一個線程能鎖定成功,如果沒有鎖成功,那么流程將卡在lock()這里不斷嘗試去鎖定。所以我們在使用??的時候要注意盡量在lock()和unlock()中間插入較小的代碼片段,這樣才能提高多線程時程序的執(zhí)行效率,比如前面的加加操作,只有一行,如果你在鎖住某個線程之后后面在unlock之前還讓線程睡眠了一會,那你可真是個大聰明??。當(dāng)然也有其他的方法可以跳過等待,后面我們也會說到。

\(\quad\)在使用互斥量的時候要包含頭文件 #include<mutex> 然后使用mutex 類即可創(chuàng)建對象。更重要的一點是,在代碼中 lock() (上鎖)和 unlock() (解鎖)必須成對使用,代碼中使用互斥量的時絕不允許非對稱調(diào)用,即 lock() 和 unlock() 一定是成對出現(xiàn)的。步驟如下:

  • 先 lock() 上鎖;
  • 然后操作共享數(shù)據(jù);
  • 再 unlock() 解鎖

2.0其他C++11新特性

2.1 std::lock_guard類模板

\(\quad\)我們在代碼中上鎖后,一定要記得解鎖,如果忘記解鎖會導(dǎo)致程序運行異常,而且通常很難排查。為了防止開發(fā)者忘記解鎖,C++11引入了一個叫做 std::lock_guard 的類模板,它在開發(fā)者忘記解鎖的時候,會替開發(fā)者自動解鎖。std::lock_guard 可以直接取代 lock() 和 unlock(),也就說使用 std::lock_guard 后,就不能再使用 lock() 和 unlock() 了。

如下所示

std::mutex var_mutex;
int i = 0;
const int maxCnt = 1000000;
void mythread()
{
  for (int j = 0; j < maxCnt; j++)
  {
    lock_guard<mutex> guard(var_mutex);
    i++;  // 線程同時操作變量
  }
}
  • 輸出結(jié)果為

i=2000000
time: 0.102605s

\(\quad\)std::lock_guard 雖然用起來方便,但是不夠靈活,它只能在析構(gòu)函數(shù)中 unlock(),也就是對象被釋放的時候,這通常是在函數(shù)返回的時候,或者通過添加代碼塊 { /* 代碼塊 */ } 限定作用域來指定釋放時機。其還有一個特性是在構(gòu)造的時候可以傳入第二個參數(shù)為std::adopt_lock,此時在析構(gòu)的時候就不會unlock了,但是此時就必須我們手動unlock了,這種使用場景也不多。

2.2 死鎖

\(\quad\)談到互斥量,就不得不來說一下死鎖。一個簡單的例子:

  • 張三在北京說:等李四來了之后,我就去廣東。
  • 李四在廣東說:等張三來了之后,我就去北京。

\(\quad\)張三李四互相扯皮,兩人一直互相等待,就死等(??)。同理,假設(shè)代碼中有兩把鎖,至少有兩個互斥量存在才會產(chǎn)生死鎖,分別稱為鎖1、鎖2,并且有兩個線程分別稱為線程A和線程B。只有在某個線程同時獲得鎖1和鎖2時,才能完成某項工作:

  • 線程A執(zhí)行時,先上鎖1------------再上鎖2。
  • 線程B執(zhí)行時,先上鎖2------------再上鎖1。

\(\quad\)如果在執(zhí)行線程A的時候,先對1上了鎖,這時候出現(xiàn)了上下文切換(并不是說上鎖之后該線程不會被其他線程占用,而是說其他線程執(zhí)行到需要鎖1的時候如果發(fā)現(xiàn)被鎖了,會給出執(zhí)行權(quán)),現(xiàn)在來到了線程B,線程需要對鎖2上鎖,進行數(shù)據(jù)操作,此時發(fā)現(xiàn)鎖2沒有被鎖,則上鎖,繼續(xù)執(zhí)行發(fā)現(xiàn),需要上鎖1,但是此時的鎖1被線程A鎖上了,于是只能給出執(zhí)行權(quán)限,此時又回到了線程1,線程1發(fā)現(xiàn)我如果想繼續(xù)執(zhí)行,那么就又要給2上鎖,但是發(fā)現(xiàn)鎖2又被線程B給上鎖了,于是也只好給出執(zhí)行權(quán)限。就這樣,兩個線程來回扯皮,就形成了死鎖。

\(\quad\)用一句話概括以下呢就是:在程序執(zhí)行線程A的過程中,上好了鎖1后,出現(xiàn)了上下文切換,系統(tǒng)調(diào)度轉(zhuǎn)去執(zhí)行線程B,把鎖2給上了,那么后續(xù)線程A拿不到鎖2,線程B拿不到鎖1,兩條線程都沒法往下執(zhí)行,即出現(xiàn)了死鎖。

  • 例如下面的程序
#include <pthread.h>
#include <atomic>
#include <chrono>
#include <iostream>
#include <mutex>
#include <thread>
#include <list>
using namespace std;
list<int> msgRecvQueue; // 容器(實際上是雙向鏈表):存放玩家發(fā)生命令的隊列 
mutex m_mutex1; // 創(chuàng)建互斥量1 
mutex m_mutex2; // 創(chuàng)建互斥量2 
void inMsgRecvQueue()
{
  for (int i = 0; i < 100000; ++i)
  {
    cout << "inMsgRecvQueue exec, push an elem " << i << endl;
    m_mutex1.lock(); // 實際代碼中,兩把鎖不一定同時上,它們可能保護不同的數(shù)據(jù)
    m_mutex2.lock();
    msgRecvQueue.push_back(i); // 假設(shè)數(shù)字 i 就是收到的玩家命令
    m_mutex2.unlock();
    m_mutex1.unlock();
  }
}
bool outMsgLULProc(int &command)
{
  m_mutex2.lock();
  m_mutex1.lock();
  if (!msgRecvQueue.empty())
  {
    command = msgRecvQueue.front(); // 返回第一個元素 
    msgRecvQueue.pop_front();       // 移除第一個元素 
    m_mutex1.unlock();
    m_mutex2.unlock();
    return true;
  }
  m_mutex1.unlock();
  m_mutex2.unlock();
  return false;
}
void outMsgRecvQueue()
{
  int command = 0;
  for (int i = 0; i < 100000; ++i)
  {
    bool result = outMsgLULProc(command);
    if (result)
      cout << "outMsgLULProc exec, and pop_front: " << command << endl;
    else
      cout << "outMsgRecvQueue exec, but queue is empty!" << i << endl;
    cout << "outMsgRecvQueue exec end!" << i << endl;
  }
}
int main()
{
	thread myInMsgObj(inMsgRecvQueue);
	thread myOutMsgObj(outMsgRecvQueue);
	myInMsgObj.join();
	myOutMsgObj.join();
	cout << "Hello World!" << endl;
	return 0;
}

筆者運行的時候發(fā)現(xiàn)程序會卡死,無法輸出最后的一句話

outMsgLULProc exec, and pop_front: 271
outMsgRecvQueue exec end!289
outMsgLULProc exec, and pop_front: 272
outMsgRecvQueue exec end!290
inMsgRecvQueue exec, push an elem 491

通常來講,死鎖的一般解決方案,只要保證多個互斥量上鎖的順序一致,就不會出現(xiàn)死鎖,比如把上面示例代碼的兩個線程回調(diào)函數(shù)中的上鎖順序改一下,保持一致就好了(都改為先上鎖1,再上鎖2)。讀者可以自己試一下改動下代碼。

  • 線程A執(zhí)行時,先上鎖1------------再上鎖2。
  • 線程B執(zhí)行時,先上鎖1------------再上鎖2。

這樣的順序之下就形不成死鎖了。因為當(dāng)切換到B的時候B由于沒有鎖1所以值接讓出執(zhí)行權(quán)限。

2.3 死鎖的另一種解決方案

std::lock() 函數(shù)模板是C++11引入的,它能一次鎖住兩個或兩個以上的互斥量,并且它不存在上述的在多線程中由于上鎖順序問題造成的死鎖現(xiàn)象,原因如下:std::lock() 函數(shù)模板在鎖定兩個互斥量時,只有兩種情況:

  • 兩個互斥量都沒有鎖住;
  • 兩個互斥量都被鎖住。

如果只鎖了一個,另一個沒鎖成功,則它會立即把已經(jīng)鎖住的互斥量解鎖。將上面的接收函數(shù)改為如下就可以避免死鎖的出現(xiàn)。

void inMsgRecvQueue()
{
  for (int i = 0; i < 100000; ++i)
  {
    cout << "inMsgRecvQueue exec, push an elem " << i << endl;
    // m_mutex1.lock(); // 實際代碼中,兩把鎖不一定同時上,它們可能保護不同的數(shù)據(jù)
    // m_mutex2.lock();
    std::lock(m_mutex1,m_mutex2);
    msgRecvQueue.push_back(i); // 假設(shè)數(shù)字 i 就是收到的玩家命令
    m_mutex2.unlock();
    m_mutex1.unlock();
  }
}

在使用 std::lock() 函數(shù)模板鎖上多個互斥量時,也必須得記得把每個互斥量解鎖,此時借助 std::lock_guard 的 std::adopt_lock 參數(shù)可以省略解鎖的代碼。我們再稍微更改一下代碼,讓他看上去更modern一些。

void inMsgRecvQueue()
{
  for (int i = 0; i < 100000; ++i)
  {
    cout << "inMsgRecvQueue exec, push an elem " << i << endl;
    // m_mutex1.lock(); // 實際代碼中,兩把鎖不一定同時上,它們可能保護不同的數(shù)據(jù)
    // m_mutex2.lock();
    std::lock(m_mutex1, m_mutex2);									 // 鎖上兩個互斥量 
    std::lock_guard<std::mutex> m_guard1(m_mutex1, std::adopt_lock); // 構(gòu)造時不上鎖,但析構(gòu)時解鎖 
    std::lock_guard<std::mutex> m_guard2(m_mutex2, std::adopt_lock); // 構(gòu)造時不上鎖,但析構(gòu)時解鎖 
    msgRecvQueue.push_back(i); // 假設(shè)數(shù)字 i 就是收到的玩家命令
  }
}

Reference

到此這篇關(guān)于C++多線程中互斥量的使用的文章就介紹到這了,更多相關(guān)C++多線程互斥量內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評論