C++ 執行緒鎖 mutex 理解 附程式碼

2020-08-12 20:55:12

1. 概述

C++裏面的mutex相關知識進行梳理,理解執行緒鎖

2. 理解

mutex英文含義如下,表示一種不相容的性質。

mutex
n. 互斥;互斥元,互斥體;互斥量
  • C++裏面的mutex類是用來進行執行緒同步保護數據的,防止不同線程對同一數據同時進行處理。如果不使用執行緒鎖會使程式不穩定,達不到預期的效果。
  • 它實際上是用來對執行緒進行處理的,不是直接處理數據不同的執行緒只有拿到了執行緒鎖纔可以繼續執行,否則需要等待其他執行緒解除執行緒鎖。
  • 執行緒進行同步時需要進行執行緒鎖的加鎖解鎖的操作。如果加鎖之後沒有及時解鎖可能會導致死鎖現象。因爲其他執行緒會等待其他執行緒解除執行緒鎖,如果一直等不到,可能會一直處於阻塞的狀態。

boost中的鎖有

		class mutex
	    typedef mutex try_mutex;
    	class timed_mutex
        typedef unique_lock<mutex> scoped_lock;
        typedef detail::try_lock_wrapper<mutex> scoped_try_lock;
        typedef unique_lock<timed_mutex> scoped_timed_lock;
        typedef detail::try_lock_wrapper<timed_mutex> scoped_try_lock;
        typedef scoped_timed_lock scoped_lock;       

3. boost::mutex

C++使用執行緒鎖時最簡單的就是使用boost::mutex。

3.1 無鎖

如果不使用執行緒鎖,程式碼範例如下
使用命令

 g++ nomutextest.cpp -o nomutex -lboost_system -lboost_thread
#include <boost/thread/thread.hpp> 
#include <iostream> 
#include <iomanip>

#include <unistd.h>

/* 
 * g++ nomutextest.cpp -o nomutex -lboost_system -lboost_thread
*/

int num = 5;
void helloA() 
{ 
        std::cout << "****I'm thread A ! " << boost::this_thread::get_id()  << " --- Start " << std::endl; 

        std::cout << "****I'm thread A the num is "<<num <<std::endl;

        num++;

        std::cout << "****I'm thread A the num+1 is "<<num <<std::endl;
        num++;

        std::cout << "****I'm thread A the num+1+1 is "<<num <<std::endl;

        sleep(1);

        std::cout << "****I'm thread A !  --- OVER " << std::endl; 

} 


  void helloB() 
{ 
        std::cout << "I'm thread B ! " << boost::this_thread::get_id()  << " --- Start " << std::endl; 

        std::cout << "I'm thread B the num is "<<num <<std::endl;

        num--;

        std::cout << "I'm thread B the num-1 is "<<num <<std::endl;
        num--;

        std::cout << "I'm thread B the num-1-1 is "<<num <<std::endl;


        std::cout << "I'm thread B !  --- OVER " << std::endl; 


} 

int main(int argc, char* argv[]) 
{ 
        // 建立並執行兩個執行緒

        boost::thread thrdA(&helloA);


        boost::thread thrdB(&helloB);  


        thrdA.join(); // 等待子執行緒完成後再繼續執行主進程;


        thrdB.join();


        // 等待兩個 join 後纔會繼續執行
        std::cout<< " ==== over ==== "<<std::endl;

        return 0; 
} 

由於執行緒實際執行時對num處理的先後順序不一樣導致結果不一樣。
可能thrdA先對num進行加處理,可以允許
也有可能thrdB先對num進行減處理,可以允許
也有可能thrdA、thrdB對num同時進行處理。禁止這種情況
輸出結果可能爲下面 下麪情況

執行緒A先對num進行處理

A5 -> A6 -> A7->B7 -> B6 -> B5

****I'm thread A ! 7f01551c6700 --- Start I'm thread B ! 
****I'm thread A the num is 5
****I'm thread A the num+1 is 6
****I'm thread A the num+1+1 is 7
7f01549c5700 --- Start 
I'm thread B the num is 7
I'm thread B the num-1 is 6
I'm thread B the num-1-1 is 5
I'm thread B !  --- OVER 
****I'm thread A !  --- OVER 
 ==== over ==== 

或者

執行緒B先對num進行處理,

B5 -> B4 -> B3 -> A3 -> A4 -> A5

I'm thread B ! 7ff008235700 --- Start 
I'm thread B the num is 5
I'm thread B the num-1 is 4
I'm thread B the num-1-1 is 3
I'm thread B !  --- OVER 
****I'm thread A ! 7ff008a36700 --- Start 
****I'm thread A the num is 3
****I'm thread A the num+1 is 4
****I'm thread A the num+1+1 is 5
****I'm thread A !  --- OVER 
 ==== over ==== 

或者

執行緒A、B同時對num進行處理。

A5 -> A6 -> B6 -> B5 -> B4 -> A5

****I'm thread A ! I'm thread B ! 7f1c044567007f1c04c57700 --- Start  --- Start 
****I'm thread A the num is 5
****I'm thread A the num+1 is 
I'm thread B the num is 6
I'm thread B the num-1 is 5
6I'm thread B the num-1-1 is 4
I'm thread B !  --- OVER 

****I'm thread A the num+1+1 is 5
****I'm thread A !  --- OVER 
 ==== over ==== 

以及其他情況,就不一一列舉了。
A5 -> B5 -> B4 -> B3 -> A4 -> A5

****I'm thread A ! 7fed8d265700 --- Start 
I'm thread B ! 7fed8ca64700****I'm thread A the num is  --- Start 
I'm thread B the num is 5
5I'm thread B the num-1 is 4
I'm thread B the num-1-1 is 3
I'm thread B !  --- OVER 

****I'm thread A the num+1 is 4
****I'm thread A the num+1+1 is 5
****I'm thread A !  --- OVER 
 ==== over ==== 

3.2 有鎖

如果使用執行緒鎖,範例程式碼如下

#include <boost/thread/thread.hpp> 
#include <boost/thread/mutex.hpp> //定義鎖
#include <iostream> 

#include <unistd.h>

/* 
 * g++ mutextest.cpp -o mutextest -lboost_system -lboost_thread
*/

boost::mutex lock; //互斥鎖

using namespace std;
int num = 5;
void helloA() 
{ 
        std::cout << "****I'm thread A ! " << boost::this_thread::get_id()  << " --- Start " << std::endl; 

        lock.lock(); // 鎖住變數 num, 另一處呼叫將在此處執行完後再繼續執行
        std::cout << "****I'm thread A the num is "<<num <<std::endl;

        num++;

        std::cout << "****I'm thread A the num+1 is "<<num <<std::endl;
        num++;

        std::cout << "****I'm thread A the num+1+1 is "<<num <<std::endl;

        sleep(1);

        lock.unlock();

        std::cout << "****I'm thread A !  --- OVER " << std::endl; 

} 


  void helloB() 
{ 
        std::cout << "I'm thread B ! " << boost::this_thread::get_id()  << " --- Start " << std::endl; 

        lock.lock(); 
        std::cout << "I'm thread B the num is "<<num <<std::endl;

        num--;

        std::cout << "I'm thread B the num-1 is "<<num <<std::endl;
        num--;

        std::cout << "I'm thread B the num-1-1 is "<<num <<std::endl;

        sleep(1);

        lock.unlock();

        std::cout << "I'm thread B !  --- OVER " << std::endl; 

} 

int main(int argc, char* argv[]) 
{ 
        // 建立並執行兩個執行緒

        boost::thread thrdA(&helloA);

        boost::thread thrdB(&helloB);  
	
	    thrdB.join();
        thrdA.join(); // 等待子執行緒完成後再繼續執行主進程;

        // 等待兩個 join 後纔會繼續執行
        std::cout<< " ==== over ==== "<<std::endl;

        return 0; 
} 

輸出結果只可能爲下面 下麪情況,
每次num的處理都是在同一個執行緒裡,如果thrdA處理num,那麼thrdB就不能處理num,反之同理。
因此不會出現執行緒A、執行緒B同時對num進行處理的情況

執行緒A先對num進行處理。

A5 -> A6 -> A7 -> B7 -> B6 -> B5

****I'm thread A ! 7f9335fd1700 --- Start 
I'm thread B ! 7f93357d0700 --- Start 
****I'm thread A the num is 5
****I'm thread A the num+1 is 6
****I'm thread A the num+1+1 is 7
****I'm thread A !  --- OVER 
I'm thread B the num is 7
I'm thread B the num-1 is 6
I'm thread B the num-1-1 is 5
I'm thread B !  --- OVER 
 ==== over ====

執行緒B先對num進行處理。

B5 -> B4 -> B3 -> A3 -> A4 -> A5

****I'm thread A ! I'm thread B ! 7f30f291f700 --- Start 
7f30f3120700I'm thread B the num is  --- Start 
5
I'm thread B the num-1 is 4
I'm thread B the num-1-1 is 3
I'm thread B !  --- OVER 
****I'm thread A the num is 3
****I'm thread A the num+1 is 4
****I'm thread A the num+1+1 is 5
****I'm thread A !  --- OVER 
 ==== over ==== 

3.3 mutex類

檢視原始碼,可以看到mutex本身還是使用pthread_mutex_t實現執行緒鎖。主要有lock(),unlock(),try_lock()三個成員函數。

class mutex
    {
    private:
        pthread_mutex_t m;
    public:
        BOOST_THREAD_NO_COPYABLE(mutex)

        mutex()
        {
            //初始化mutex
            int const res=pthread_mutex_init(&m,NULL);
            if(res)
            {
                boost::throw_exception(thread_resource_error(res, "boost:: mutex constructor failed in pthread_mutex_init"));
            }
        }
        ~mutex()
        {
          //釋放mutex
          int const res = posix::pthread_mutex_destroy(&m);
          boost::ignore_unused(res);
          BOOST_ASSERT(!res);
        }

        void lock()
        {
            //加鎖mutex
            int res = posix::pthread_mutex_lock(&m);
            if (res)
            {
                boost::throw_exception(lock_error(res,"boost: mutex lock failed in pthread_mutex_lock"));
            }
        }

        void unlock()
        {
            //解鎖mutex
            int res = posix::pthread_mutex_unlock(&m);
            (void)res;
            BOOST_ASSERT(res == 0);
//            if (res)
//            {
//                boost::throw_exception(lock_error(res,"boost: mutex unlock failed in pthread_mutex_unlock"));
//            }
        }

        bool try_lock()
        {
            int res;
            do
            {
                //嘗試加鎖mutex
                res = pthread_mutex_trylock(&m);
            } while (res == EINTR);
            if (res==EBUSY)
            {
                return false;
            }

            return !res;
        }

對於pthread_mutex_t的使用主要有下面 下麪幾條:

函數:

  • pthread_mutex_init(pthread_mutex_t * mutex, const phtread_mutexattr_t * mutexattr); //動態方式建立鎖,相當於new動態建立一個物件
  • pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //以靜態方式建立鎖
  • pthread_mutex_destory(pthread_mutex_t *mutex) //釋放互斥鎖,相當於delete
  • pthread_mutex_lock(pthread_mutex_t *mutex)
  • pthread_mutex_unlock(pthread_mutex_t *mutex)
  • int pthread_mutex_trylock(pthread_mutex_t * mutex); //會嘗試對mutex加鎖。如果mutex之前已經被鎖定,返回非0,;如果mutex沒有被鎖定,則函數返回並鎖定mutex;

pthread_mutex初始化時,需要傳入參數pthread_mutexattr_t,有下列值可選

  • PTHREAD_MUTEX_TIMED_NP,這是預設值,也就是普通鎖。當一個執行緒加鎖以後,其餘請求鎖的執行緒將形成一個等待佇列,並在解鎖後按優先順序獲得鎖。這種鎖策略保證了資源分配的公平性。
  • PTHREAD_MUTEX_RECURSIVE_NP,巢狀鎖,允許同一個執行緒對同一個鎖成功獲得多次,並通過多次unlock解鎖。如果是不同線程請求,則在加鎖執行緒解鎖時重新競爭。
  • PTHREAD_MUTEX_ERRORCHECK_NP,檢錯鎖,如果同一個執行緒請求同一個鎖,則返回EDEADLK,否則與PTHREAD_MUTEX_TIMED_NP型別動作相同。這樣就保證當不允許多次加鎖時不會出現最簡單情況下的死鎖。
  • PTHREAD_MUTEX_ADAPTIVE_NP,適應鎖,動作最簡單的鎖型別,僅等待解鎖後重新競爭。

這些都在mutex類中有使用。

typedef mutex try_mutex;

4. boost::timed_mutex

時間鎖timed_mutex,與mutex相比,可以設定加鎖時等待的絕對時間、相對時間,超時自動退出,不再加鎖。

  • 時間鎖使用:在指定時間之內進行加鎖,如果不能加鎖,自動退出

列舉重要的函數如下

  • bool timed_lock(TimeDuration const & relative_time)相對時間鎖
  • bool timed_lock(boost::xtime const & absolute_time)絕對時間鎖
  • bool timed_lock(system_time const & abs_time)絕對時間鎖
class timed_mutex
    {
    private:
        pthread_mutex_t m;
        、、、
    public:
       template<typename TimeDuration>
       bool timed_lock(TimeDuration const & relative_time)
       {
           return timed_lock(get_system_time()+relative_time);
       }
       bool timed_lock(boost::xtime const & absolute_time)
       {
           return timed_lock(system_time(absolute_time));
       }
       bool timed_lock(system_time const & abs_time)
        {
            struct timespec const ts=boost::detail::to_timespec(abs_time);
            return do_try_lock_until(ts);
        }
    private:
        bool do_try_lock_until(struct timespec const &timeout)
        {
          int const res=pthread_mutex_timedlock(&m,&timeout);
          BOOST_ASSERT(!res || res==ETIMEDOUT);
          return !res;
        }

5. boost::mutex::scoped_lock

可以自動加鎖,自動解鎖,不用手動進行這些操作,作用範圍是my_lock的作用域。構造時自動上鎖,解構時自動解鎖
使用unique_lock來實現這個功能。如下

typedef unique_lock<mutex> scoped_lock;

unique_lock的內部結構包含了一個唯一的mutex型別,這個可以是時間鎖、普通鎖等。將裏面重要的資訊列舉如下

  • bool owns_lock()是否有鎖
  • lock()加鎖,建構函式自動加鎖
  • unlock()解鎖,解構函式自動解鎖
  • try_lock()嘗試加鎖
  • swap()交換鎖以及鎖的狀態
  template <typename Mutex>
  class unique_lock
  {
  private:
    Mutex* m;
    bool is_locked;
  private:
    explicit unique_lock(upgrade_lock<Mutex>&);
    unique_lock& operator=(upgrade_lock<Mutex>& other);
  public:
    explicit unique_lock(Mutex& m_) :
      m(&m_), is_locked(false)
    {
      lock();
    }
    bool owns_lock() const BOOST_NOEXCEPT
    {
      return is_locked;
    }
    void lock()
    {
      if (m == 0)
      {
        boost::throw_exception(
            boost::lock_error(static_cast<int>(system::errc::operation_not_permitted), "boost unique_lock has no mutex"));
      }
      if (owns_lock())
      {
        boost::throw_exception(
            boost::lock_error(static_cast<int>(system::errc::resource_deadlock_would_occur), "boost unique_lock owns already the mutex"));
      }
      m->lock();
      is_locked = true;
    }
    bool try_lock()
    {
      if (m == 0)
      {
        boost::throw_exception(
            boost::lock_error(static_cast<int>(system::errc::operation_not_permitted), "boost unique_lock has no mutex"));
      }
      if (owns_lock())
      {
        boost::throw_exception(
            boost::lock_error(static_cast<int>(system::errc::resource_deadlock_would_occur), "boost unique_lock owns already the mutex"));
      }
      is_locked = m->try_lock();
      return is_locked;
    }
    ~unique_lock()
    {
      if (owns_lock())
      {
        m->unlock();
      }
    }
    void swap(unique_lock& other)BOOST_NOEXCEPT
    {
      std::swap(m,other.m);
      std::swap(is_locked,other.is_locked);
    }

5.1 範例

boost::mutex::scoped_lock my_lock (lock);

等價與之前的

lock.lock();
、、、
lock.unlock();

6. std::mutex

除了boost庫C++11中也包含了mutex類
C++11中新增了<mutex>標頭檔案,實現了C++11標準中的一些互斥存取的類與方法等。其中std::mutex可以進行加鎖lock、解鎖unlock。也可以自動加鎖解鎖,std::lock_guard與std::mutex配合使用,把鎖放到lock_guard中時,mutex自動上鎖,lock_guard解構時,同時把mutex解鎖

  • lock_guard:更加靈活的鎖管理類別範本,構造時是否加鎖是可選的,在物件解構時如果持有鎖會自動釋放鎖,所有權可以轉移。物件生命期內允許手動加鎖和釋放鎖

  • scope_lock:嚴格基於作用域(scope-based)的鎖管理類別範本,構造時是否加鎖是可選的(不加鎖時假定當前執行緒已經獲得鎖的所有權),解構時自動釋放鎖,所有權不可轉移,物件生存期內不允許手動加鎖和釋放鎖

  • share_lock:用於管理可轉移和共用所有權的互斥物件。

std::lock_guardstd::unique_lockstd::shared_lock類別範本在構造時是否加鎖是可選的,可選參數有

參數 功能
(預設) 請求鎖阻塞當前執行緒直到成功獲得鎖
std::defer_lock 不請求鎖
std::try_to_lock 嘗試請求鎖,但不阻塞執行緒,鎖不可用時也會立即返回。
std::adopt_lock 假定當前執行緒已經獲得互斥物件的所有權,所以不再請求鎖

7. 死鎖

7.1 範例

一個死鎖範例如下

std::mutex mt1, mt2;
// thread 1
{
    std::lock_guard<std::mutex> lck1(mt1);
    std::lock_guard<std::mutex> lck2(mt2);
    // do something
}
// thread 2
{
    std::lock_guard<std::mutex> lck2(mt2);
    std::lock_guard<std::mutex> lck1(mt1);
    // do something
}

可能會出現

  • thread 1持有mt1等待mt2,
  • thread 2持有mt2等待mt1,

死鎖出現,爲了避免這種情況,對於任意兩個互斥物件,在多個執行緒中進行加鎖時應保證其先後順序是一致

std::mutex mt1, mt2;
// thread 1
{
    std::lock_guard<std::mutex> lck1(mt1);
    std::lock_guard<std::mutex> lck2(mt2);
    // do something
}
// thread 2
{
    std::lock_guard<std::mutex> lck1(mt1);
    std::lock_guard<std::mutex> lck2(mt2);
    // do something
}

也可以使用std::lockstd::try_lock函數來對多個Lockable物件加鎖。

  • std::try_lock不會阻塞執行緒當有物件不可用時會釋放已經加鎖的其他物件並立即返回。
  • 當待加鎖的物件中有不可用物件時std::lock會阻塞當前執行緒知道所有物件都可用。該函數使用未指定的呼叫序列來鎖定物件的成員lock、try_lock和unlock,以確保在返回時鎖定所有參數(不會產生任何死鎖)。如果函數不能鎖定所有物件(例如,因爲它的一個內部呼叫拋出了一個異常),函數在失敗之前首先解鎖它成功鎖定的所有物件(如果有的話)。

7.2 範例

防止死鎖的程式碼如下
使用命令

g++ nodeadlock.cpp -o nodeadlock -std=c++11  -lpthread
#include <thread>
#include <mutex>
#include <iostream>
#include <unistd.h>
int g_i = 5;

std::mutex mt1;  //
std::mutex mt2;  //

//g++ nodeadlock.cpp -o nodeadlock -std=c++11  -lpthread


void testA()
{
    std::lock(mt1, mt2);//去掉將會死鎖
    std::cout<<"****test A g_i "<<g_i<<std::endl;
    std::lock_guard<std::mutex> lck1(mt1,std::adopt_lock);//這個時候已經獲得了鎖,因此使用adopt_lock
    g_i++;
    sleep(2);
    std::cout<<"****test A g_i+1 "<<g_i<<std::endl;
    std::lock_guard<std::mutex> lck2(mt2,std::adopt_lock);
    g_i++;
    std::cout<<"****test A g_i+1+1 "<<g_i<<std::endl;
}

void testB()
{
    std::lock(mt1, mt2);
    std::cout<<"test B g_i "<<g_i<<std::endl;
    std::lock_guard<std::mutex> lck2(mt2,std::adopt_lock);
    g_i--;
    sleep(2);
    std::cout<<"test B g_i-1 "<<g_i<<std::endl;
    std::lock_guard<std::mutex> lck1(mt1,std::adopt_lock);
    g_i--;
    std::cout<<"test B g_i-1-1 "<<g_i<<std::endl;
}
 
int main()
{
    std::thread t1(testA);
    std::thread t2(testB);
 
    t1.join();
    t2.join();
    std::cout<<"=====over====="<<std::endl;
} 

輸出結果爲,已經沒有了死鎖

****test A g_i 5
****test A g_i+1 6
****test A g_i+1+1 7
test B g_i 7
test B g_i-1 6
test B g_i-1-1 5
=====over=====

7.3 防止死鎖

三種用於避免死鎖的技術:

  加鎖順序(執行緒按照一定的順序加鎖)
  按照順序加鎖是一種有效的死鎖預防機制 機製。但是,這種方式需要你事先知道所有可能會用到的鎖並對這些鎖做適當的排序,但總有些時候是無法預知的。
 加鎖時限(執行緒嘗試獲取鎖的時候加上一定的時限,超過時限則放棄對該鎖的請求,並釋放自己佔有的鎖)
  也可能是因爲獲得了鎖的執行緒(導致其它執行緒超時)需要很長的時間去完成它的任務。
死鎖檢測

8. 參考鏈接

  1. C++ 執行緒鎖理解
  2. Boost Thread 臨界區 mutex
  3. C++11中std::mutex的使用
  4. C++11中std::unique_lock的使用
  5. scope_lock與lock_guard區別
  6. std::unique_lock