日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

C++?多線程之互斥量(mutex)詳解_C 語言

作者:胖小迪 ? 更新時間: 2022-04-28 編程語言

C++ 11中的互斥量,聲明在??頭文件中,互斥量的使用可以在各種方面,比較常用在對共享數據的讀寫上,如果有多個線程同時讀寫一個數據,那么想要保證多線程安全,就必須對共享變量的讀寫進行保護(上鎖),從而保證線程安全。

互斥量主要有四中類型:

  • std::mutex,最基本的 Mutex 類。
  • std::recursive_mutex,遞歸 Mutex 類。
  • std::time_mutex,限時 Mutex 類。
  • std::recursive_timed_mutex,限時遞歸 Mutex 類。

當然C++14和C++17各增加了一個:

  • std::shared_timed_mutex,限時讀寫鎖(C++14)
  • std::shared_mutex,讀寫鎖(C++17)

std::mutex

構造函數

mutex();
mutex(const mutex&) = delete;

從上面的構造函數可以看出,std::mutex不允許拷貝構造,當然也不允許move,最初構造的mutex對象是處于未鎖定狀態的,若構造不成功會拋出?std::system_error?。

析構函數

~mutex();

銷毀互斥。若互斥被線程占有,或在占有mutex時線程被終止,則會產生未定義行為。

lock

void lock();

鎖定互斥,調用線程將鎖住該互斥量。線程調用該函數會發生下面 3 種情況:

  • 如果該互斥量當前沒有被其他線程鎖住,則調用線程將該互斥量鎖住,直到調用unlock之前,該線程一直擁有該鎖。
  • 如果當前互斥量被其他線程鎖住,則當前的調用線程被阻塞住,指導其他線程unlock該互斥量。
  • 如果當前互斥量被當前調用線程鎖住,則會產生死鎖(deadlock)。

try_lock

bool try_lock();

嘗試鎖住互斥量,立即返回。成功獲得鎖時返回 true ,否則返回 false。

如果互斥量被其他線程占有,則當前線程也不會被阻塞。線程調用該函數也會出現下面 3 種情況:

  • 如果當前互斥量沒有被其他線程占有,則該線程鎖住互斥量,直到該線程調用 unlock 釋放互斥量。
  • 如果當前互斥量被其他線程鎖住,則當前調用線程返回 false,而并不會被阻塞掉。
  • 如果當前互斥量被當前調用線程鎖住,則會產生死鎖(deadlock)。

unlock

void unlock();

解鎖互斥。互斥量必須為當前執行線程所鎖定(以及調用lock),否則行為未定義。

看下面一個簡單的例子實現兩個線程競爭全局變量g_num對其進行寫操作,然后打印輸出:

#include 
#include   // std::chrono
#include   // std::thread
#include   // std::mutex
int g_num = 0;  // 為 g_num_mutex 所保護
std::mutex g_num_mutex;
void slow_increment(int id) 
{
    for (int i = 0; i < 3; ++i) {
        g_num_mutex.lock();
        ++g_num;
        std::cout << "th" << id << " => " << g_num << '\n';
        g_num_mutex.unlock();
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
}  
int main()
{
    std::thread t1(slow_increment, 0);
    std::thread t2(slow_increment, 1);
    t1.join();
    t2.join();
}

加了互斥量實現有序的寫操作并輸出:

th0 => 1
th1 => 2
th0 => 3
th1 => 4
th1 => 5
th0 => 6

如果不增加mutex包含,可能輸出就不是有序的打印1到6,如下:

  • thth01 => 2 => 2
  • th1 => 3
  • th0 => 4
  • th0 => 5
  • th1 => 6

std::recursive_mutex

如上面所說的,如果使用std::mutex,如果一個線程在執行中需要再次獲得鎖,會出現死鎖現象。要避免這種情況下就需要使用遞歸式互斥量std::recursive_mutex,它不會產生上述的死鎖問題,可以理解為同一個線程多次獲得鎖“僅僅增加鎖的計數”,同時,必須要確保unlock和lock的次數相同,其他線程才可能取得這個mutex。它的接口與std::mutex的完全一樣,用法也基本相同除了可重入(必須同一線程才可重入,其他線程需等待),看下面的例子:

#include 
#include 
#include 
class X {
    std::recursive_mutex m;
    std::string shared;
  public:
    void fun1() {
      m.lock();
      shared = "fun1";
      std::cout << "in fun1, shared variable is now " << shared << '\n';
      m.unlock();
    }
    void fun2() {
      m.lock();
      shared = "fun2";
      std::cout << "in fun2, shared variable is now " << shared << '\n';
      fun3(); // 遞歸鎖在此處變得有用
      std::cout << "back in fun2, shared variable is " << shared << '\n';
      m.unlock();
    }
    void fun3() {
      m.lock();
      shared = "fun3";
      std::cout << "in fun3, shared variable is now " << shared << '\n';
      m.unlock();
    }
};
int main() 
{
    X x;
    std::thread t1(&X::fun1, &x);
    std::thread t2(&X::fun2, &x);
    t1.join();
    t2.join();
}

在fun2中調用fun3,而fun3中還使用了lock和unlock,只有遞歸式互斥量才能滿足當前情況。

輸出如下:

in fun1, shared variable is now fun1
in fun2, shared variable is now fun2
in fun3, shared variable is now fun3
back in fun2, shared variable is fun3

std::time_mutex

timed_mutex增加了帶時限的try_lock。即try_lock_fortry_lock_until

try_lock_for嘗試鎖互斥。阻塞直到超過指定的?timeout_duration?或得到鎖,取決于何者先到來。成功獲得鎖時返回 true,否則返回false 。函數原型如下:

template< class Rep, class Period >
bool try_lock_for( const std::chrono::duration& timeout_duration );

timeout_duration小于或等于timeout_duration.zero(),則函數表現同try_lock()。由于調度或資源爭議延遲,此函數可能阻塞長于timeout_duration

#include 
#include 
#include 
#include 
#include 
#include 
std::timed_mutex mutex;
using namespace std::chrono_literals;
void do_work(int id) {
  std::ostringstream stream;
  for (int i = 0; i < 3; ++i) {
    if (mutex.try_lock_for(100ms)) {
      stream << "success ";
      std::this_thread::sleep_for(100ms);
      mutex.unlock();
    } else {
      stream << "failed ";
    }
    std::this_thread::sleep_for(100ms);
  }
  std::cout << "[" << id << "] " << stream.str() << std::endl;
}
int main() {
  // try_lock_for
  std::vector threads;
  for (int i = 0; i < 4; ++i) {
    threads.emplace_back(do_work, i);
  }
  for (auto& t : threads) {
    t.join();
  }
}

[3] failed success failed?
[0] success failed success?
[2] failed failed failed?
[1] success success success?

try_lock_until也是嘗試鎖互斥。阻塞直至抵達指定的timeout_time或得到鎖,取決于何者先到來。成功獲得鎖時返回 true,否則返回false。

timeout_time與上面的timeout_duration不一樣,timeout_duration表示一段時間,比如1秒,5秒或者10分鐘,而timeout_time表示一個時間點,比如說要等到8點30分或10點24分才超時。

使用傾向于timeout_time的時鐘,這表示時鐘調節有影響。從而阻塞的最大時長可能小于但不會大于在調用時的 timeout_time - Clock::now() ,依賴于調整的方向。由于調度或資源爭議延遲,函數亦可能阻塞長于抵達timeout_time之后。同try_lock(),允許此函數虛假地失敗并返回false,即使在?timeout_time?前的某點任何線程都不鎖定互斥。函數原型如下:

template< class Clock, class Duration >
bool try_lock_until( const std::chrono::time_point& timeout_time);

看下面的例子:

#include 
#include 
#include 
#include 
#include 
#include 
std::timed_mutex mutex;
using namespace std::chrono;
void do_work() {
    mutex.lock();
    std::cout << "thread 1, sleeping..." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(4));
    mutex.unlock();
}
void do_work2() {
    auto now = std::chrono::steady_clock::now();
    if (mutex.try_lock_until(now + 5s)) {
        auto end = steady_clock::now();
        std::cout << "try_lock_until success, ";
        std::cout << "time use: " << duration_cast(end-now).count() 
            << "ms." << std::endl;
        mutex.unlock();
    } else {
        auto end = steady_clock::now();
        std::cout << "try_lock_until failed, ";
        std::cout << "time use: " << duration_cast(end-now).count() 
            << "ms." << std::endl;
    }
}
int main() {
  // try_lock_until
  std::thread t1(do_work);
  std::thread t2(do_work2);
  t1.join();
  t2.join();
}

獲得鎖時輸出:

thread 1, sleeping...
try_lock_until success, time use: 4000ms.

修改一下,讓其超時,輸出:

thread 1, sleeping...
try_lock_until failed, time use: 5000ms.

std::recursive_timed_mutex

以類似std::recursive_mutex的方式,recursive_timed_mutex提供排他性遞歸鎖,同線程可以重復獲得鎖。另外,recursive_timed_mutex通過try_lock_fortry_lock_until方法,提供帶時限地獲得recursive_timed_mutex鎖,類似std::time_mutex

std::shared_mutex

c++ 17 新出的具有獨占模式和共享模式的鎖。共享模式能夠被std::shared_lock(這個后面再詳細將)占有。

std::shared_mutex 是讀寫鎖,把對共享資源的訪問者劃分成讀者和寫者,讀者只對共享資源進行讀訪問,寫者則需要對共享資源進行寫操作。

它提供兩種訪問權限的控制:共享性(shared)和排他性(exclusive)。通過lock/try_lock獲取排他性訪問權限(僅有一個線程能占有互斥),通過lock_shared/try_lock_shared獲取共享性訪問權限(多個線程能共享同一互斥的所有權)。這樣的設置對于區分不同線程的讀寫操作特別有用。

std::shared_mutex通常用于多個讀線程能同時訪問同一資源而不導致數據競爭,但只有一個寫線程能訪問的情形。比如,有多個線程調用shared_mutex.lock_shared(),多個線程都可以獲得鎖,可以同時讀共享數據,如果此時有一個寫線程調用?shared_mutex.lock(),則讀線程均會等待該寫線程調用shared_mutex.unlock()。對于C++11 沒有提供讀寫鎖,可使用?boost::shared_mutex

std::shared_mutex新增加的三個接口:

void lock_shared();
bool try_lock_shared();
void unlock_shared();

一個簡單例子如下:

#include 
#include   // 對于 std::unique_lock
#include 
#include 
class ThreadSafeCounter {
 public:
  ThreadSafeCounter() = default;
  // 多個線程/讀者能同時讀計數器的值。
  unsigned int get() const {
    std::shared_lock lock(mutex_);
    return value_;
  }
  // 只有一個線程/寫者能增加/寫線程的值。
  void increment() {
    std::unique_lock lock(mutex_);
    value_++;
  }
  // 只有一個線程/寫者能重置/寫線程的值。
  void reset() {
    std::unique_lock lock(mutex_);
    value_ = 0;
  }
 private:
  mutable std::shared_mutex mutex_;
  unsigned int value_ = 0;
};
int main() {
  ThreadSafeCounter counter;
  auto increment_and_print = [&counter]() {
    for (int i = 0; i < 3; i++) {
      counter.increment();
      std::cout << std::this_thread::get_id() << ' ' << counter.get() << '\n';
      // 注意:寫入 std::cout 實際上也要由另一互斥同步。省略它以保持示例簡潔。
    }
  };
  std::thread thread1(increment_and_print);
  std::thread thread2(increment_and_print);
  thread1.join();
  thread2.join();
}
// 解釋:下列輸出在單核機器上生成。 thread1 開始時,它首次進入循環并調用 increment() ,
// 隨后調用 get() 。然而,在它能打印返回值到 std::cout 前,調度器將 thread1 置于休眠
// 并喚醒 thread2 ,它顯然有足夠時間一次運行全部三個循環迭代。再回到 thread1 ,它仍在首個
// 循環迭代中,它最終打印其局部的計數器副本的值,即 1 到 std::cout ,再運行剩下二個循環。
// 多核機器上,沒有線程被置于休眠,且輸出更可能為遞增順序。

可能的輸出:

139847802500864 1
139847802500864 2
139847802500864 3
139847794108160 4
139847794108160 5
139847794108160 6

std::shared_timed_mutex

它是從C++14 才提供的限時讀寫鎖:std::shared_timed_mutex

對比std::shared_mutex新增下面兩個接口,其實這兩個接口與上面講到的std::timed_mutextry_lock_fortry_lock_until類似。都是限時等待鎖。只不過是增加了共享屬性。

template< class Rep, class Period >
bool try_lock_shared_for( const std::chrono::duration& timeout_duration );
template< class Clock, class Duration >
bool try_lock_shared_until( const std::chrono::time_point& timeout_time );

總結

由于它們額外的復雜性,讀/寫鎖std::shared_mutex?,?std::shared_timed_mutex優于普通鎖std::mutexstd::timed_mutex的情況比較少見。但是理論上確實存在。

如果在頻繁但短暫的讀取操作場景,讀/寫互斥不會提高性能。它更適合于讀取操作頻繁且耗時的場景。當讀操作只是在內存數據結構中查找時,很可能簡單的鎖會勝過讀/寫鎖。

如果讀取操作的開銷非常大,并且您可以并行處理許多操作,那么在某些時候增加讀寫比率應該會導致讀取/寫入器性能優于排他鎖的情況。斷點在哪里取決于實際工作量。

另請注意,在持有鎖的同時執行耗時的操作通常是一個壞兆頭。可能有更好的方法來解決問題,然后使用讀/寫鎖。

還要注意,在使用mutex時,要時刻注意lock()與unlock()的加鎖臨界區的范圍,不能太大也不能太小,太大了會導致程序運行效率低下,大小了則不能滿足我們對程序的控制。并且我們在加鎖之后要及時解鎖,否則會造成死鎖,lock()與unlock()應該是成對出現。

原文鏈接:https://blog.csdn.net/iuices/article/details/123099747

欄目分類
最近更新