## 1、使用互斥量 在C++中,我們通過構造`std::mutex`的實例來創建互斥量,調用成員函數`lock()`對其加鎖,調用`unlock()`解鎖。但通常更推薦的做法是使用標準庫提供的類模板`std::lock_guard`,它針對互斥量實現了RAII手法:在構造時給互斥量加鎖,析構時解鎖 ...
1、使用互斥量
在C++中,我們通過構造std::mutex
的實例來創建互斥量,調用成員函數lock()
對其加鎖,調用unlock()
解鎖。但通常更推薦的做法是使用標準庫提供的類模板std::lock_guard<>
,它針對互斥量實現了RAII手法:在構造時給互斥量加鎖,析構時解鎖。兩個類都在頭文件<mutex>
里聲明。
std::list<int> some_list;
std::mutex some_mutex;
void add_to_list(int value)
{
//C++17引入了類模板參數推導的新特性,所以下麵語句也可以簡化成:std::lock_guard guard(some_mutex);
std::lock_guard<std::mutex> guard(some_mutex);
some_list.push_back(value);
}
bool list_contains(int value)
{
std::lock_guard<std::mutex> guard(some_mutex);
return std::find(some_list.begin(), some_list.end(), value) != some_list.end();
}
2、防範死鎖
假設有兩個線程,都需要同時鎖住兩個互斥量才能進行某種操作,但它們分別隻鎖住了一個互斥量,都等著再給另一個互斥量加鎖,這就構成了死鎖。標準庫提供了std::lock
函數來解決死鎖的問題,它可以同時鎖住多個互斥量。
class some_big_object {};
void swap(some_big_object& lhs, some_big_object& rhs) {}
class X
{
private:
some_big_object some_detail;
mutable std::mutex m;
public:
X(const some_big_object& 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::mutex
對象上加鎖,那麼再次試圖加鎖將導致未定義的行為。構造std::lock_guard
對象時,額外參數std::adopt_lock
指明互斥量已被鎖住,std::lock_guard
實例應當據此接收鎖的歸屬權,不得在構造函數內試圖另行加鎖。
針對上述場景,C++17還提供了新的RAII模板類std::scoped_lock<>
,它和std::lock_guard<>
完全等價,只不過前者是可變參數模板,接收各種互斥量型別作為模板參數列表,還以多個互斥量對象作為構造函數參數列表。下列代碼中,傳入構造函數的兩個互斥量都被加鎖,機制與std::lock()函數相同,因此,當構造函數完成時它們都被鎖定,而後在析構函數內一起被解鎖。
void swap(X& lhs, X& rhs)
{
if (&lhs == &rhs) { return; }
//這裡使用了C++17的類模板參數推導特性,下麵的語句完全等價於std::scoped_lock<std::mutex, std::mutex> guard(lhs.m, rhs.m);
std::scoped_lock guard(lhs.m, rhs.m);
swap(lhs.some_detail, rhs.some_detail);
}
標準庫也提供了std::unique_lock<>
模板,它與std::lock_guard<>
一樣,也是一個以互斥量作為參數的類模板,並且以RAII手法管理鎖,不過它更靈活一些(代價是略微損失性能)。std::unique_lock<>
的構造函數接收第二個參數,我們可以傳入std::adopt_lock
以指明std::unique_lock
對象管理互斥量上的鎖,也可以傳入std::defer_lock
使互斥量在完成構造時處於無鎖狀態,等以後有需要時再加鎖。
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);
}
std::unique_lock
類十分靈活,它具有成員函數lock()
、try_lock()
、unlock()
,這與互斥量的基本成員函數一致,所以該類可以結合泛型函數來使用,例如std::lock()
。std::unique_lock
的實例可以在銷毀前通過成員函數unlock()
解鎖,這意味著如果執行流程的任何特定分支沒有必要繼續持有鎖,那我們就可以提前解鎖,這在有些情況下可能有助於提升程式性能。
鎖的歸屬權可以在多個std::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、保護共用數據的其它工具
3.1、保護共用數據的初始化
假設共用數據只在初始化過程中需要保護,此後無需再進行顯式的同步操作,那麼可以使用std::once_flag
類和std::call_once
函數來處理這種情況,它們可以保證初始化操作只會執行一次。std::once_flag
的實例既不可複製也不可移動,這與std::mutex
類似。
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();
}
C++11規定了局部靜態變數的初始化只會在某個單一線程上發生,在初始化完成之前,其它線程不會越過靜態數據的聲明而繼續運行。如果某些類只需要用到唯一一個全局實例,這種情況下可以用以下方法代替std::call_once
:
class my_class;
my_class& get_my_class_instance()
{
static my_class instance;
return instance;
}
3.2、保護不常更新的數據
如果我們想要允許單獨一個“寫線程”進行完全排他的訪問,也允許多個“讀線程”共用數據或併發訪問,那麼可以使用C++17提供的新互斥量std::shared_mutex
。對於更新操作,使用std::lock_guard<std::shared_mutex>
或std::unique_lock<std::shared_mutex>
鎖定,代替對應的std::mutex
特化,它們都保證了訪問的排他性質。對於無需更新數據結構的線程,可以另行改用共用鎖std::shared_lock<std::shared_mutex>
,多個線程能夠同時鎖住同一個std::shared_mutex
。
class dns_entry {};
class dns_cache
{
std::map<std::string, dns_entry> entries;
std::shared_mutex entry_mutex;
public:
dns_entry find_entry(const std::string& domain)
{
std::shared_lock<std::shared_mutex> lk(entry_mutex);
auto it = entries.find(domain);
return it == entries.end() ? dns_entry() : it->second;
}
void update_or_add_entry(const std::string& domain, const dns_entry& dns_details)
{
std::lock_guard<std::shared_mutex> lk(entry_mutex);
entries[domain] = dns_details;
}
};
3.3、遞歸加鎖
標準庫提供了std::recursive_mutex
,其工作方式與std::mutex
相似,不同之處是其允許同一線程對某互斥量的同一實例多次加鎖。假如我們對它調用3次lock()
,就必須調用3次unlock()
才能解鎖。