1. 程式人生 > >c++互斥鎖mutex使用簡介

c++互斥鎖mutex使用簡介

1. 多個執行緒訪問同一資源時,為了保證資料的一致性,最簡單的方式就是使用 mutex(互斥鎖)。

引用 cppreference 的介紹:

1

The mutex class is a synchronization primitive that can be used to protect shared data from being simultaneously accessed by multiple threads.

方法1:直接操作 mutex,即直接呼叫 mutex 的 lock / unlock 函式
此例順帶使用了 boost::thread_group

 來建立一組執行緒。

#include <iostream>
#include <boost/thread/mutex.hpp>
#include <boost/thread/thread.hpp>

boost::mutex mutex;
int count = 0;

void Counter() {
  mutex.lock();

  int i = ++count;
  std::cout << "count == " << i << std::endl;

  // 前面程式碼如有異常,unlock 就調不到了。
  mutex.unlock();
}

int main() {
  // 建立一組執行緒。
  boost::thread_group threads;
  for (int i = 0; i < 4; ++i) {
    threads.create_thread(&Counter);
  }

  // 等待所有執行緒結束。
  threads.join_all();
  return 0;
}

方法2:使用 lock_guard 自動加鎖、解鎖。原理是 RAII,和智慧指標類似

#include <iostream>
#include <boost/thread/lock_guard.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/thread/thread.hpp>

boost::mutex mutex;
int count = 0;

void Counter() {
  // lock_guard 在建構函式里加鎖,在解構函式裡解鎖。
  boost::lock_guard<boost::mutex> lock(mutex);

  int i = ++count;
  std::cout << "count == " << i << std::endl;
}

int main() {
  boost::thread_group threads;
  for (int i = 0; i < 4; ++i) {
    threads.create_thread(&Counter);
  }

  threads.join_all();
  return 0;
}

方法3:使用 unique_lock 自動加鎖、解鎖
unique_lock 與 lock_guard 原理相同,但是提供了更多功能(比如可以結合條件變數使用)。
注意:mutex::scoped_lock 其實就是 unique_lock<mutex> 的 typedef

#include <iostream>
#include <boost/thread/mutex.hpp>
#include <boost/thread/thread.hpp>

boost::mutex mutex;
int count = 0;

void Counter() {
  boost::unique_lock<boost::mutex> lock(mutex);

  int i = ++count;
  std::cout << "count == " << i << std::endl;
}

int main() {
  boost::thread_group threads;
  for (int i = 0; i < 4; ++i) {
    threads.create_thread(&Counter);
  }

  threads.join_all();
  return 0;
}

方法4:為輸出流使用單獨的 mutex
這麼做是因為 IO 流並不是執行緒安全的!
如果不對 IO 進行同步,此例的輸出很可能變成:

count == count == 2count == 41
count == 3

因為在下面這條輸出語句中:

std::cout << "count == " << i << std::endl;

輸出 "count == " 和 i 這兩個動作不是原子性的(atomic),可能被其他執行緒打斷。

#include <iostream>
#include <boost/thread/mutex.hpp>
#include <boost/thread/thread.hpp>
#include <boost/thread/lock_guard.hpp>

boost::mutex mutex;
boost::mutex io_mutex;
int count = 0;

void Counter() {
  int i;
  {
    boost::unique_lock<boost::mutex> lock(mutex);
    i = ++count;
  }

  {
    boost::unique_lock<boost::mutex> lock(io_mutex);
    std::cout << "count == " << i << std::endl;
  }
}

int main() {
  boost::thread_group threads;
  for (int i = 0; i < 4; ++i) {
    threads.create_thread(&Counter);
  }

  threads.join_all();
  return 0;
}

2. 保護共享資料的替代設施

2.1 保護共享資料的初始化過程

醜陋的程式碼:

void undefined_behaviour_with_double_checked_locking()
{
    if(!resource_ptr) // 1
    {
       std::lock_guard<std::mutex> lk(resource_mutex);
        if(!resource_ptr) // 2
        {
          resource_ptr.reset(new some_resource); // 3
        }
    }
    resource_ptr->do_something(); // 4
}    

這個模式為什麼聲名狼藉呢?因為這裡有潛在的條件競爭,因為外部的讀取鎖①沒有與內部的
寫入鎖進行同步③。因此就會產生條件競爭,這個條件競爭不僅覆蓋指標本身,還會影響到其
指向的物件;即使一個執行緒知道另一個執行緒完成對指標進行寫入,它可能沒有看到新建立的
some_resource例項,然後呼叫do_something()④後,得到不正確的結果。

C++標準庫提供了 std::once_flag 和 std::call_once 來處理這種情況。比起鎖住互斥量,並顯式的檢查指

針,每個執行緒只需要使用 std::call_once ,在 std::call_once 的結束時,就能安全的知道指
針已經被其他的執行緒初始化了。使用 std::call_once 比顯式使用互斥量消耗的資源更少,特
別是當初始化完成後。

std::shared_ptr<some_resource> resource_ptr;
std::once_flag resource_flag; // 1
void init_resource()
{
resource_ptr.reset(new some_resource);
}
void foo()
{
std::call_once(resource_flag,init_resource); // 可以完整的進行一次初始化
resource_ptr->do_something();
}

2.2 保護很少更新的資料結構

雖然更新頻度很低,但更新也是有可能發生的,並且當這個可快取被多個執行緒訪問,這個緩
存就需要適當的保護措施,來對其處於更新狀態時進行保護,也為了確保執行緒讀到快取中的
有效資料。

使用一
個 std::mutex 來保護資料結構,這的確有些反應過度,因為在沒有發生修改時,它將削減並
發讀取資料的可能性;這裡需要另一種不同的互斥量。這種新的互斥量常被稱為“讀者-寫者
鎖”(reader-writer mutex),因為其允許兩中不同的使用方式:一個“作者”執行緒獨佔訪問和共
享訪問,讓多個“讀者”執行緒併發訪問。

新的C++標準庫應該不提供這樣的互斥量,Boost庫提供了boost::shared_mutex。

3.3 巢狀鎖

C++標準庫提供了 std::recursive_mutex 類。其功能與 std::mutex 類似,除了你可以從
同一執行緒的單個例項上獲取多個鎖。在互斥量鎖住其他執行緒前,你必須釋放你擁有的所有
鎖,所以當你呼叫lock()三次時,你也必須呼叫unlock()三次。正確使
用 std::lock_guard<std::recursive_mutex> 和 std::unique_lock<std::recursice_mutex> 可以幫
你處理這些問題。