線程基本的互斥和同步工具類, 主要包括:
std::mutex 類
std::recursive_mutex 類
std::timed_mutex 類
std::recursive_timed_mutex 類
std::lock_guard 類型模板
std::unique_lock 類型模板
std::lock 函數模板
std::once_flag 類
std::call_once 函數模板
std::mutex 上鎖須要調用 lock() 或 try_lock(), 當有一個線程獲取了鎖, 其它線程想要取得此對象的鎖時, 會被阻塞(lock)或失敗(try_lock). 當線程完成共享數據的保護後, 需要調用 unlock 進行釋放鎖.
std::mutex 不支嵌套, 如果兩次調用 lock, 會產生未定義行為.
使用方法同 std::mutex, 但 std::recursive_mutex 支持一個線程獲取同一個互斥量多次,而沒有對其進行一次釋放. 但是同一個線程內, lock 與 unlock 次數要相等, 否則其它線程將不能取得任何機會.
其原理是, 調用 lock 時, 當調用線程已持有鎖時, 計數加1; 調用 try_lock 時, 嘗試取得鎖, 失敗時不會阻塞, 成功時計數加1; 調用 unlock 時, 計數減1, 如果是最後一個鎖時, 釋放鎖.
需要注意的是: 調用 try_lock時, 如果當前線程未取得鎖, 即使沒有別的線程取得鎖, 也有可能失敗.
std::timed_mutex 在 std::mutex 的基礎上支持讓鎖超時. 上鎖時可以調用 try_lock_for, try_lock_until 設置超時值.
try_lock_for 的參數是需要等待的時間, 當參數小於等於0時會立即返回, 效果和使用 try_lock 一樣.
try_lock_until 傳入的參數不能小於當前時間, 否則會立即返回, 效果和使用 try_lock 一樣. 實際上 try_lock_for 內部也是調用 try_lock_until 實現的.
tm.try_lock_for(std::chrono::milliseconds(1000)) 與 tm.try_lock_until(std::chrono::steady_clock::now() + std::chrono::milliseconds(1000)) 等價, 都是等待1s.
std::recursive_timed_mutex 在 std::recursive_mutex 的基礎上, 讓鎖支持超時.
用法同 std::timed_mutex, 超時原理同 std::recursive_mutex.
std::lock_guard 類型模板為基礎鎖包裝所有權. 指定的互斥量在構造函數中上鎖, 在析構函數中解鎖.
這就為互斥量鎖部分代碼提供了一個簡單的方式: 當程序運行完成時, 阻塞解除, 互斥量解鎖(無論是執行到最後, 還是通過控制流語句break或return, 亦或是拋出異常).
std::lock_guard 不支持拷貝構造, 拷貝賦值和移動構造.
std::unique_lock 類型模板比 std::loc_guard 提供了更通用的所有權包裝器.
std::unique_lock 可以調用 unlock 釋放鎖, 而後當再次需要對共享數據進行訪問時再調用 lock(), 但是必須注意一次 lock 對應一次 unlock, 不能連續多次調用同一個 lock 或 unlock.
std::unique_lock 不支持拷貝構造和拷貝賦值, 但是支持移動構造和移動賦值.
std::unique_lock 比 std::loc_guard 還增加了另外幾種構造方式:
unique_lock(_Mutex& _Mtx, adopt_lock_t) 構建持有鎖實例, 其不會調用 lock 或 try_lock, 但析構時默認會調用 unlock.
unique_lock(_Mutex& _Mtx, defer_lock_t) 構建非持有鎖實例, 其不會調用 lock 或 try_lock, 如果沒有使用 std::lock 等函數修改標志, 析構時也不會調用 unlock.
unique_lock(_Mutex& _Mtx, try_to_lock_t) 嘗試從互斥量上獲取鎖, 通過調用 try_lock
unique_lock(_Mutex& _Mtx, const chrono::duration<_Rep, _Period>& _Rel_time) 在給定時間長度內嘗試獲取鎖
unique_lock(_Mutex& _Mtx, const chrono::time_point<_Clock, _Duration>& _Abs_time) 在給定時間點內嘗試獲取鎖
bool owns_lock() const 檢查是否擁有一個互斥量上的鎖
std::lock 函數模板提供同時鎖住多個互斥量的功能, 且不會有因改變鎖的一致性而導致的死鎖. 其聲明如下:
template<typename LockableType1,typename... LockableType2> void lock(LockableType1& m1,LockableType2& m2...);
// 使用互斥量保護代碼 typedef std::lock_guard<std::mutex> MutexLockGuard; typedef std::unique_lock<std::mutex> UniqueLockGuard; class Func { int i; std::mutex& m; public: Func(int i_, std::mutex& m_) : i(i_), m(m_) {} void operator() () { //MutexLockGuard lk(m); UniqueLockGuard lk(m); for (unsigned j = 0; j < 10; ++j) { std::cout << i << " "; } std::cout << std::endl; } }; std::mutex m; std::vector<std::thread> threads; for (int i = 1; i < 10; i++) { Func f(i, m); threads.push_back(std::thread(f)); } std::for_each(threads.begin(), threads.end(), std::mem_fn(&std::thread::join)); // 對每個線程調用join()
// 同時對多個 mutex 上鎖 std::mutex m1; std::mutex m2; //std::unique_lock<std::mutex> lock_a(m1, std::defer_lock); //std::unique_lock<std::mutex> lock_b(m2, std::defer_lock); // std::def_lock 留下未上鎖的互斥量 //std::lock(lock_a, lock_b); // 互斥量在這裡上鎖, 並修改對象的上鎖標志 std::lock(m1, m2); // 鎖住兩個互斥量 std::lock_guard<std::mutex> lock_a(m1, std::adopt_lock); // std::adopt_lock 參數表示對象已經上鎖,因此不會調用 lock 函數 std::lock_guard<std::mutex> lock_b(m2, std::adopt_lock);
如果多個線程需要同時調用某個函數,std::call_once 可以保證多個線程對該函數只調用一次, 並且是線程安全的.
-- 使用 std::call_once 和 std::once_flag
考慮下面的代碼, 每個線程必須等待互斥量,以便確定數據源已經初始化, 這導致了線程資源產生不必要的序列化問題.
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(); }
使用雙重檢查鎖優化上述代碼, 指針第一次讀取數據不需要獲取鎖, 並且只有在指針為NULL時才需要獲取鎖; 然後, 當獲取鎖之後, 指針會被再次檢查一遍(這就是雙重檢查的部分), 避免另一的線程在第一次檢查後再做初始化, 並且讓當前線程獲取鎖.
這樣同樣存在問題, 即潛在的條件競爭, 因為外部的讀取鎖①時沒有與內部的寫入鎖進行同步③, 因此就會產生條件競爭,這個條件競爭不僅覆蓋指針本身, 還會影響到其指向的對象:
即使一個線程知道另一個線程完成對指針進行寫入, 它可能沒有看到新創建的some_resource實例, 然後調用do_something()④後, 得到不正確的結果. 這在C++標准中被指定為“未定義行為”.
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 }
C++的解決方法:
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(); }
線程安全類成員的延遲初始化
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) // 1 { std::call_once(connection_init_flag, &X::open_connection, this); // 2 connection.send_data(data); } data_packet receive_data() // 3 { std::call_once(connection_init_flag, &X::open_connection, this); // 2 return connection.receive_data(); } };
讀者-寫者鎖 boost::shared_lock, 允許兩中不同的使用方式:一個“作者”線程獨占訪問和共享訪問, 讓多個“讀者”線程並發訪問. (C++11標准不支持)
其性能依賴與參與其中的處理器數量, 也與讀者和寫者線程的負載有關. 一種典型的應用:
#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); // 1 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); // 2 entries[domain] = dns_details; } };