1. 程式人生 > >C++併發實戰19:lock free程式設計

C++併發實戰19:lock free程式設計

     涉及到並行/併發計算時,通常都會想到加鎖,加鎖可以保護共享的資料,不過也會存在一些問題:
1. 由於臨界區無法併發執行,進入臨界區就需要等待,加鎖使得效率的降低。多核CPU也不能發揮全部馬力
2. 在複雜的情況下,很容易造成死鎖,併發程序、執行緒之間無止境的互相等待。

3. 在中斷/訊號處理函式中不能加鎖,給併發處理帶來困難。

4. 加鎖影響實時性,等待時間不確定

5. 優先順序反轉,優先順序高的等待優先順序低的

6. 若一個執行緒帶著鎖掛了,那麼將會影響其它等待該鎖的執行緒

總之,在基於鎖的多執行緒/多程序程式設計,你需要保證對競爭條件很敏感的共享資料上的任何操作,都通過加鎖或解鎖一個獨佔(mutex)來實現原子操作。

現有的加鎖/無鎖程式設計的種類如下:

     

      其中標註為紅色字型的方案為 Blocking synchronization需要鎖,黑色字型為 Non-blocking synchronization無鎖。Lock-based 和 Lockless-based 兩者之間的區別僅僅是加鎖粒度的不同。圖中最底層的方案就是大家經常使用的 mutex 和 semaphore 等方案,程式碼複雜度低,但執行效率也最低。

      可以使用std::atomic實現lock free,但這裡並不是真正的無鎖,只有atomic_flag是無鎖的,其它的atomic內部都是有鎖的只不過粒度很小.atomic::compare_exchange_weak/strong等於是個CAS(比較並交換)操作,在C++11之前該操作是平臺相關的,現在atomic將其實現為成員函式。

      一個lock free的棧:

#include <atomic>
#include <memory>

template<typename T>
class lock_free_stack//棧的底層資料結構採用單向連結串列實現
{
private:
    struct node
    {
        std::shared_ptr<T> data;//這裡採用shared_ptr管理的好處在於:若棧記憶體放物件pop中return棧頂物件可能拷貝異常,棧內只儲存指標還可以提高效能
        node* next;
        node(T const& data_):
            data(std::make_shared<T>(data_))//注意make_shared比直接shared_ptr構造的記憶體開銷小
        {}
    };
    std::atomic<node*> head;//採用原子型別管理棧頂元素,主要利用atomic::compare_exchange_weak實現lock free
public:
    void push(T const& data)
    {
        node* const new_node=new node(data);
        new_node->next=head.load();//每次從連結串列頭插入
        while(!head.compare_exchange_weak(new_node->next,new_node));//若head==new_node->next則更新head為new_node,返回true結束迴圈,插入成功; 若head!=new_node->next表明有其它執行緒在此期間對head操作了,將new_node->next更新為新的head,返回false,繼續進入下一次while迴圈。這裡採用atomic::compare_exchange_weak比atomic::compare_exchange_strong快,因為compare_exchange_weak可能在元素相等的時候返回false所以適合在迴圈中,而atomic::compare_exchange_strong保證了比較的正確性,不適合用於迴圈
    }
    std::shared_ptr<T> pop()
    {
        node* old_head=head.load();//拿住棧頂元素,但是可能後續被更新,更新發生在head!=old_head時
        while(old_head &&!head.compare_exchange_weak(old_head,old_head->next));//這裡注意首先要先判斷old_head是否為nullptr防止操作空連結串列,然後按照compare_exchange_weak語義更新連結串列頭結點。若head==old_head則更新head為old_head->next並返回true,結束迴圈,刪除棧頂元素成功;若head!=old_head表明在此期間有其它執行緒操作了head,因此更新old_head為新的head,返回false進入下一輪迴圈,直至刪除成功。
        return old_head ? old_head->data : std::shared_ptr<T>();//這裡注意空連結串列時返回的是一個空的shared_ptr物件
    }//這裡只是lock free,由於while迴圈可能無限期迴圈不能在有限步驟內完成,故不是wait free
};