1. 程式人生 > >C++11執行緒中的幾種鎖

C++11執行緒中的幾種鎖

執行緒之間的鎖有:互斥鎖、條件鎖、自旋鎖、讀寫鎖、遞迴鎖。一般而言,鎖的功能與效能成反比。不過我們一般不使用遞迴鎖(C++標準庫提供了std::recursive_mutex),所以這裡就不推薦了。

互斥鎖(Mutex)

互斥鎖用於控制多個執行緒對他們之間共享資源互斥訪問的一個訊號量。也就是說是為了避免多個執行緒在某一時刻同時操作一個共享資源。例如執行緒池中的有多個空閒執行緒和一個任務佇列。任何是一個執行緒都要使用互斥鎖互斥訪問任務佇列,以避免多個執行緒同時訪問任務佇列以發生錯亂。

在某一時刻,只有一個執行緒可以獲取互斥鎖,在釋放互斥鎖之前其他執行緒都不能獲取該互斥鎖。如果其他執行緒想要獲取這個互斥鎖,那麼這個執行緒只能以阻塞方式進行等待。

標頭檔案:< mutex >
型別: std::mutex
用法:在C++中,通過構造std::mutex的例項建立互斥元,呼叫成員函式lock()來鎖定它,呼叫unlock()來解鎖,不過一般不推薦這種做法,標準C++庫提供了std::lock_guard類模板,實現了互斥元的RAII慣用語法。std::mutex和std::lock _ guard。都宣告在< mutex >標頭檔案中。

參考程式碼:

//用互斥元保護列表
#include <list>
#include <mutex>

std::list<int>
some_list; std::mutex some_mutex; void add_to_list(int new_value) { std::lock_guard<std::mutex> guard(some_mutex); some_list.push_back(new_value); }

條件鎖

條件鎖就是所謂的條件變數,某一個執行緒因為某個條件為滿足時可以使用條件變數使改程式處於阻塞狀態。一旦條件滿足以“訊號量”的方式喚醒一個因為該條件而被阻塞的執行緒。最為常見就是線上程池中,起初沒有任務時任務佇列為空,此時執行緒池中的執行緒因為“任務佇列為空”這個條件處於阻塞狀態。一旦有任務進來,就會以訊號量的方式喚醒一個執行緒來處理這個任務。

標頭檔案:< condition_variable >
型別:std::condition_variable(只和std::mutex一起工作) 和 std::condition_variable_any(符合類似互斥元的最低標準的任何東西一起工作)。

//使用std::condition_variable等待資料
std::mutex mut;
std::queue<data_chunk> data_queue;
std::condition_variable data_cond;

void data_preparation_thread()
{
    while(more_data_to_prepare())
    {
        data_chunk const data=prepare_data();
        std::lock_guard<std::mutex> lk(mut);
        data_queue.push(data);
        data_cond.notify_one();
    }
}

void data_processing_thread()
{
    while(true)
    {
        std::unique_lock<std::mutex> lk(mut);   //這裡使用unique_lock是為了後面方便解鎖
        data_cond.wait(lk,{[]return !data_queue.empty();});
        data_chunk data=data_queue.front();
        data_queue.pop();
        lk.unlock();
        process(data);
        if(is_last_chunk(data))
            break;
    }
}
  • wait()的實現接下來檢查條件,並在滿足時返回。如果條件不滿足,wait()解鎖互斥元,並將該執行緒置於阻塞或等待狀態。當來自資料準備執行緒中對notify_one()的呼叫通知條件變數時,執行緒從睡眠狀態中甦醒(解除其阻塞),重新獲得互斥元上的鎖,並再次檢查條件,如果條件已經滿足,就從wait()返回值,互斥元仍被鎖定。如果條件不滿足,該執行緒解鎖互斥元,並恢復等待。
  • 如果等待執行緒只打算等待一次,那麼當條件為true時它就不會再等待這個條件變量了,條件變數未必是同步機制的最佳選擇。如果等待的條件是一個特定資料塊的可用性時,這尤其正確。在這個場景中,使用期值(future)更合適。使用future等待一次性事件。

自旋鎖

前面的兩種鎖是比較常見的鎖,也比較容易理解。下面通過比較互斥鎖和自旋鎖原理的不同,這對於真正理解自旋鎖有很大幫助。

假設我們有一個兩個處理器core1和core2計算機,現在在這臺計算機上執行的程式中有兩個執行緒:T1和T2分別在處理器core1和core2上執行,兩個執行緒之間共享著一個資源。

首先我們說明互斥鎖的工作原理,互斥鎖是是一種sleep-waiting的鎖。假設執行緒T1獲取互斥鎖並且正在core1上執行時,此時執行緒T2也想要獲取互斥鎖(pthread_mutex_lock),但是由於T1正在使用互斥鎖使得T2被阻塞。當T2處於阻塞狀態時,T2被放入到等待佇列中去,處理器core2會去處理其他任務而不必一直等待(忙等)。也就是說處理器不會因為執行緒阻塞而空閒著,它去處理其他事務去了。

而自旋鎖就不同了,自旋鎖是一種busy-waiting的鎖。也就是說,如果T1正在使用自旋鎖,而T2也去申請這個自旋鎖,此時T2肯定得不到這個自旋鎖。與互斥鎖相反的是,此時執行T2的處理器core2會一直不斷地迴圈檢查鎖是否可用(自旋鎖請求),直到獲取到這個自旋鎖為止。

從“自旋鎖”的名字也可以看出來,如果一個執行緒想要獲取一個被使用的自旋鎖,那麼它會一致佔用CPU請求這個自旋鎖使得CPU不能去做其他的事情,直到獲取這個鎖為止,這就是“自旋”的含義。

當發生阻塞時,互斥鎖可以讓CPU去處理其他的任務;而自旋鎖讓CPU一直不斷迴圈請求獲取這個鎖。通過兩個含義的對比可以我們知道“自旋鎖”是比較耗費CPU的。

//使用std::atomic_flag的自旋鎖互斥實現
class spinlock_mutex
{
    std::atomic_flag flag;
public:
spinlock_mutex():flag(ATOMIC_FLAG_INIT) {}
void lock()
{
    while(flag.test_and_set(std::memory_order_acquire));
}
void unlock()
{
    flag.clear(std::memory_order_release);
}
}

讀寫鎖

說到讀寫鎖我們可以藉助於“讀者-寫者”問題進行理解。首先我們簡單說下“讀者-寫者”問題。

計算機中某些資料被多個程序共享,對資料庫的操作有兩種:一種是讀操作,就是從資料庫中讀取資料不會修改資料庫中內容;另一種就是寫操作,寫操作會修改資料庫中存放的資料。因此可以得到我們允許在資料庫上同時執行多個“讀”操作,但是某一時刻只能在資料庫上有一個“寫”操作來更新資料。這就是一個簡單的讀者-寫者模型。

標頭檔案:boost/thread/shared_mutex.cpp
型別:boost::shared_lock

用法:你可以使用boost::shared_ mutex的例項來實現同步,而不是使用std::mutex的例項。對於更新操作,std::lock_guard< boost::shared _mutex>和 std::unique _lock< boost::shared _mutex>可用於鎖定,以取代相應的std::mutex特化。這確保了獨佔訪問,就像std::mutex那樣。那些不需要更新資料結構的執行緒能夠轉而使用 boost::shared _lock< boost::shared _mutex>來獲得共享訪問。這與std::unique _lock用起來正是相同的,除了多個執行緒在同一時間,同一boost::shared _mutex上可能會具有共享鎖。唯一的限制是,如果任意一個執行緒擁有一個共享鎖,試圖獲取獨佔鎖的執行緒會被阻塞,知道其他執行緒全都撤回它們的鎖。同樣的,如果一個執行緒具有獨佔鎖,其他執行緒都不能獲取共享鎖或獨佔鎖,直到第一個執行緒撤回它的鎖。

參考BLOG