《C++ concurrency in action》 讀書筆記 -- Part 2 第三章 線程間的資料共用

來源:互聯網
上載者:User

唐風

www.cnblogs.com/liyiwen

《C++ concurreny in action》

第三章 sharing data between threads

3.1 線程間共用資料的“問題”

invariants 被破壞(比如說一個讀一個寫)

3.1.1 race conditions

條件競爭是:

In concurrency, a race condition is anything where the outcome depends on the relative ordering of execution of operations on two or more threads; the threads race to perform their respective operations.

多線程程式的主要問題之一: race condition

避免race condition的方法:

  1. 設計保持機制保證同一時刻只有一個線程在對資料做修改(以及修改時其它線程不會訪問到它) => 使用mutex
  2. 使用 lock-free 的方式(這個在第7章)
3.2 C++ 標準中的 mutex

mutex(mutual exclusion)

mutex會是C++中最常用的資料保護手段,但它們也不是銀彈,重要的是你要組織好你的代碼以實現正確的(合適粒度的)資料保護,mutex也有自身的問題:比如死結,或是保護過多或是過少(粒度不對會出現“同步保護失去意義”,或是系統的整體效能變差)。

mutex的基本操作:lock()鎖定,unlock()解鎖

為了避免在各種退出中unlock資源,C++提供了std::lock_guard這個模板,用RAII實現了“安全的解鎖”

在std::lock_guard的建構函式中會lock,解構函式中會unlock

基本例子:

#include <list>#include <mutex>#include <algorithm>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);}bool list_contains(int value_to_find) {    std::lock_guard<std::mutex> guard(some_mutex);    return std::find(some_list.begin(),some_list.end(),value_to_find) != some_list.end();}

如果把資料封裝好(成為private成員),在操作的時候lock,結束操作的時候unlock,那麼一般來說資料就得到了比較好的保護。但是,這也還會有其它的問題:

Any code that has access to that pointer or reference can now access (and potentially modify) the protected data without locking the mutex.

要想做好資料保護,就必須仔細地設計API,以使得任何資料在被訪問之前都能夠鎖定。

class some_data{    int a;    std::string b;public:    void do_something();};class data_wrapper{private:    some_data data;    std::mutex m;public:    template<typename Function>    void process_data(Function func)    {         std::lock_guard<std::mutex> l(m);        func(data);    }};some_data* unprotected;void malicious_function(some_data& protected_data){    unprotected=&protected_data;}data_wrapper x;void foo(){     x.process_data(malicious_function);     unprotected->do_something();}

像這樣一段代碼,就很容易地刺穿了鎖地保護。而這一切,C++無法幫到你,只有夠程式員自己認真的設計程式的結構。一般來說應該盡量地遵守:

Don’t pass pointers and references to protected data outside the scope of the lock, whether by returning them from a function, storing them in externally visible memory, or passing them as arguments to user-supplied functions.

這個規則

3.2.3 Spotting race conditions inherent in interfaces

介面帶來的race condistion。本節舉了個stack例作為例子,

stack<int> s;if(!s.empty()) {    int const value=s.top();     s.pop();     do_something(value);}

像這樣一個簡單常用的情境下就有多個線程間的race condition問題。(就算stack內部已經做了很好的保護了)

比如statck中只有一個元素了,兩個線程都進行上面代碼的操作的話,就有可能A線程調用完empty判斷後,B也調用這個函數,結果兩個線程都進入了if語句塊,而pop兩次。

除了這個問題之外,還有別的問題,比如有多個線程進行中作業,由stack來儲存所有的任務,每個線程完成一個工作後就從stack中去取一個新的任務,這樣的話,如果出現下面這樣的調度結果就會導致兩個線程做了同一個任務,並且又丟掉一個任務誰都沒有做。

所以對於介面本身存在的這種多線程可能導致的不安全性,必須要進行良好的設計和使用規範,才可能做到安全執行緒。

本書對pop這個函數的設計改進做了一些舉例。

我:每種不同的情境都會有不同的對應方法,沒有走遍天的一招鮮,主要還是要考慮:出現異常情況下的資料保護是否OK,連續的API調用對線程同步有什麼要求,之類的

3.2.4 死結(DeadLock)

死結是多線程編程最大的問題之一。最簡單的死結就是兩個線程相互等待對方釋放資源,結果誰都得不到資源,僵死在那。

防止死結最簡單的手段就是:多個鎖的鎖定一定要按相同的順序來。但這也不是包治百病的神藥。Listing3.6做了說明,swap函數如果簡單地實現為“按固定順序對要交換的兩個對象進行鎖定,然後再交換這兩個對象”的話,那麼在不同線程中進行這個操作,如果給的參數順序不一致,還是會引起死結。C++給出了“同時鎖定若干個鎖”的函數:std::lock (all or nothing)

(A mutex that does permit multiple locks by the same thread is provided in the form of std::recursive_mutex. See section 3.3.3 for details.) )

class some_big_object;void swap(some_big_object& lhs,some_big_object& rhs);class X{private:    some_big_object some_detail;    std::mutex m;public:    X(some_big_object const& sd):some_detail(sd){}    friend void swap(X& lhs, X& rhs)    {        if(&lhs==&rhs)            return;        std::lock(lhs.m,rhs.m);         std::lock_guard<std::mutex> lock_a(lhs.m,std::adopt_lock);         std::lock_guard<std::mutex> lock_b(rhs.m,std::adopt_lock);         swap(lhs.some_detail,rhs.some_detail);    }};

std::lock(lhs.m,rhs.m); 可以同時鎖定兩個鎖。

std::lock_guard<std::mutex> lock_a(lhs.m,std::adopt_lock); std::adopt_lock這個選項告訴lock_guard在構造的時候不要去鎖定mutex,因為可能之前已經鎖定了。

3.2.5 進階的“防死結”準則

除了mutex會造成死結外,還有其它的可能性,比如線程相互等待對方結束,總而言之一句話,“不要相互等待”

a) 避免鎖嵌套

-> 不要重複鎖定一個鎖

-> 用std::lock 去“同時”鎖定多個鎖

b) 避免調用 外部提供的帶鎖的函數

因為你不知道外部提供的函數是怎麼操作鎖的,所以很容易出問題,當然,寫庫或是一些很泛化的代碼的時候無法避免地要調用外部提供的函數。這時就要一些制定一些準則進行遵守。

c) 以固定的順序進行鎖定

d) USE A LOCK HIERARCHY,軟體上自己設計lock的層次。並以此來管理鎖定關係。

3.2.6 std::unique_lock

它也可以在獵取mutex時不進行鎖定,但比起前面的std::adopt選項來說,它的差別在於,

  • unique_lock可以在之後再進行鎖定(lock_guard的adpot之後就不再能去鎖定了)??? 但unique_lock可以,所以在鎖定時間點上更有彈性
  • unique_lock可以傳遞mutex的所有權。

缺點:它更慢,也更大(因為內部要儲存mutex是否已經鎖定的狀態)

使用例:

class X{private:    some_big_object some_detail;    std::mutex m;public:    X(some_big_object const& sd):some_detail(sd){}    friend void swap(X& lhs, X& rhs)    {        if(&lhs==&rhs)             return;        std::unique_lock<std::mutex> lock_a(lhs.m,std::defer_lock);         std::unique_lock<std::mutex> lock_b(rhs.m,std::defer_lock);         std::lock(lock_a,lock_b);         swap(lhs.some_detail,rhs.some_detail);    }};

3.2.7 傳遞mutext的所有權(用unique_lock)

這個很簡單,例子如下:

std::unique_lock<std::mutex> get_lock(){    extern std::mutex some_mutex;    std::unique_lock<std::mutex> lk(some_mutex);    prepare_data();    return lk; }void process_data(){    std::unique_lock<std::mutex> lk(get_lock());     do_something();}

3.2.8

注意鎖的粒度(包括保護的資料的大小和只要必要的時候鎖定)

以不必要的時間期間鎖定mutex顯示會帶來效能影響(別的線程在等待)

3.3 其它的同步保護方法

3.3.1 :只在初始化時進行同步保護

有些資料只需要在初始化時候進行保護,初始化完成以後就不再需要同步保護了(比如已經變成唯讀了),這種情況下,如果不進行保護的話,會出現race condition,如果提供保護呢,其實除了第一次的鎖定是必要的之外,之後的訪問帶來的加鎖解鎖除了增加了開銷沒有任何用處。

std::shared_ptr<some_resource> resource_ptr;std::mutex resource_mutex;void foo(){    std::unique_lock<std::mutex> lk(resource_mutex);     if(!resource_ptr)    {        resource_ptr.reset(new some_resource);     }    lk.unlock();    resource_ptr->do_something();}

為了盡量的減少開銷,人們還想了類似“double-check lock”的手法

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

但這也有問題,因為(1)和第(3)步之間沒有同步保護的機制,所以可能在一個線程運行到第2)步使得resource_ptr不為空白,但又沒有完成初始化時,另一個線程過了(1)而直接調用了(3),但由於resource_ptr沒有完全初始化成功,所以(3)的調用結果是未定義的,很可能是個錯誤值。

為瞭解決這兩難的問題,C++11提供了這種在初始化時對資料進行同步保護的方法。

std::shared_ptr<some_resource> resource_ptr;std::once_flag resource_flag; void init_resource(){    resource_ptr.reset(new some_resource); }void foo(){    std::call_once(resource_flag,init_resource);     resource_ptr->do_something();}

std::call_once加std::once_flag可以保護只需要做一次的操作受到同步保護,而且在做完之後的其它情況下的訪問不會有像mutex的lock那樣“重的開銷”,因為帶來了平衡。

class X{private:    connection_info connection_details;    connection_handle connection;    std::once_flag connection_init_flag;    void open_connection()    {        connection=connection_manager.open(connection_details);    }public:    X(connection_info const& connection_details_):    connection_details(connection_details_)    {}    void send_data(data_packet const& data)     {        std::call_once(connection_init_flag,&X::open_connection,this);         connection.send_data(data);    }    data_packet receive_data()     {        std::call_once(connection_init_flag,&X::open_connection,this);         return connection.receive_data();    }};

這個例子則表示了怎麼用std::call_once和std::once_flag怎麼對類成員進行保護。

另外:

class my_class;my_class& get_my_class_instance(){    static my_class instance;     return instance;}

這種用法,在C++11中可以保護my_class在第一個線程第一次調用時,完成初始化(而且是可以同步保護的!!在完成初始化之前其它的線程如果調用這個函數,會卡在這裡)。這在C++11之前是不能保護安全執行緒的。

3.3.2 Protecting rarely updated data structures

還有一種資料,對這種資料極少進行寫操作,大部分時間都是讀操作,顯然,在讀操作的時候是不需要進行同步保護的,只有在有寫操作時才需要進行鎖定,避免多個寫或是寫時其它線程讀出“不完整的值”。對於這種rarely updated的資料,如果用一般的mutex來進行保護,則比較“費”,因為讀時的鎖定保護是沒有什麼用處的。如果有一種鎖,可以在“各線程都只進行了讀操作的情況”下,不進行鎖定,但一旦有一個線程進行寫操作,則這個鎖會保證“一旦我Lock了,就意味著我獨佔了資料,只到我unlock為止,誰也沒辦法再通過這個lock”的話,那麼我們就又一次在“安全和高效能上”取得了平衡。

可惜的是,這個鎖的提案在C++11中被否決了。

幸運的是,boost裡有這個鎖。

例子:

#include <map>#include <string>#include <mutex>#include <boost/thread/shared_mutex.hpp>class dns_entry;class dns_cache{    std::map<std::string,dns_entry> entries;    mutable boost::shared_mutex entry_mutex;public:    dns_entry find_entry(std::string const& domain) const    {        boost::shared_lock<boost::shared_mutex> lk(entry_mutex);         std::map<std::string,dns_entry>::const_iterator const it=        entries.find(domain);        return (it==entries.end())?dns_entry():it->second;    }    void update_or_add_entry(        std::string const& domain,        dns_entry const& dns_details)    {        std::lock_guard<boost::shared_mutex> lk(entry_mutex);         entries[domain]=dns_details;    }};

3.3.3 Recursive locking

mutex是不可以重複進行鎖定的,而且一般來說一個好的設計也不會出現對鎖進行重複lock的情況,但是,凡事有萬一,萬一你真的需要,那麼就用:std::recursive_mutex,當然一定要記得,lock了幾次就得unloc幾次,否則別的線程就無法去佔有資源了。

當然,這個std::recursive_mutex也是可以和mutex一樣,放到

std::lock_guard<std::recursive_mutex>and std::unique_lock<std::recursive_mutex>中,利用RAII來進行解鎖的。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.