歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux編程 >> Linux編程 >> C++並發編程 互斥和同步

C++並發編程 互斥和同步

日期:2017/3/1 9:08:33   编辑:Linux編程

C++並發編程 異步任務(async)

線程基本的互斥和同步工具類, 主要包括:
  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 類

  std::mutex 上鎖須要調用 lock() 或 try_lock(), 當有一個線程獲取了鎖, 其它線程想要取得此對象的鎖時, 會被阻塞(lock)或失敗(try_lock). 當線程完成共享數據的保護後, 需要調用 unlock 進行釋放鎖.
  std::mutex 不支嵌套, 如果兩次調用 lock, 會產生未定義行為.

std::recursive_mutex 類

使用方法同 std::mutex, 但 std::recursive_mutex 支持一個線程獲取同一個互斥量多次,而沒有對其進行一次釋放. 但是同一個線程內, lock 與 unlock 次數要相等, 否則其它線程將不能取得任何機會.
其原理是, 調用 lock 時, 當調用線程已持有鎖時, 計數加1; 調用 try_lock 時, 嘗試取得鎖, 失敗時不會阻塞, 成功時計數加1; 調用 unlock 時, 計數減1, 如果是最後一個鎖時, 釋放鎖.
需要注意的是: 調用 try_lock時, 如果當前線程未取得鎖, 即使沒有別的線程取得鎖, 也有可能失敗.

std::timed_mutex 類

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_timed_mutex 在 std::recursive_mutex 的基礎上, 讓鎖支持超時.
用法同 std::timed_mutex, 超時原理同 std::recursive_mutex.

std::lock_guard 類型模板

std::lock_guard 類型模板為基礎鎖包裝所有權. 指定的互斥量在構造函數中上鎖, 在析構函數中解鎖.
這就為互斥量鎖部分代碼提供了一個簡單的方式: 當程序運行完成時, 阻塞解除, 互斥量解鎖(無論是執行到最後, 還是通過控制流語句break或return, 亦或是拋出異常).
std::lock_guard 不支持拷貝構造, 拷貝賦值和移動構造.

std::unique_lock 類型模板

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 函數模板

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::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

讀者-寫者鎖 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;
        }
    };
    

Copyright © Linux教程網 All Rights Reserved