線程的互斥和同步(8)- C++11中的互斥鎖和條件變量


1. 互斥鎖

之前講過使用Windows的API和Qt中如何創建和使用互斥鎖。接下來,主要說明一下C++11中的互斥鎖。
c++11中的互斥鎖主要有如下幾種:

互斥鎖 說明
mutex 最基本的互斥鎖,不可重入
timed_mutex 具有超時機制的互斥鎖
recursive_mutex 可重入的互斥鎖
recursive_timed_mutex 結合 timed_mutex 和 recursive_mutex 特點的互斥量

他們都有加鎖(lock)、嘗試加鎖(trylock)和解鎖(unlock)的方法。

(1) 遞歸鎖和非遞歸鎖

這裏說明一下 遞歸鎖非遞歸鎖

  • 遞歸鎖 ,可重入鎖。表示同一線程中,可以多次加鎖,要解鎖相應的次數才能夠解鎖。
  • 非遞歸鎖 ,不可重入。表示如果多次加鎖,會阻塞導致線程死鎖。

Windows API CreateCreateMutex 和臨界區都屬於 遞歸鎖
而Qt中的 QMutex 創建時默認是 非遞歸的鎖 ,構造中傳入參數 Recursive (可遞歸),如果不傳則默認爲 NonRecursive (非遞歸)。

接下來舉一個例子來說明:

std::mutex g_mutex;
int g_number = 0;

void func2(void);
void func(void)
{
	g_mutex.lock();
	++g_number;
	func2();
	g_mutex.unlock();
}

void func2(void)
{
	g_mutex.lock();
	++g_number;
	g_mutex.unlock();
}

如果在線程中,調用函數 func() 就會引起線程 死鎖 ,因爲它加鎖兩次。
而使用 std::recursive_mutex 替換 std::mutex 就沒有死鎖的問題。
但是,這並不意味着,我們就需要使用 遞歸鎖 ,使用 非遞歸鎖 更容易定位我們程序中的問題。

(2) 互斥鎖管理類

之前我們在j講 QMutex 的時候,同時講解了RALL的 QMutexLocker ;C++11中也提供了類似的方法。
Qt中的互斥鎖可參照:線程的互斥和同步(4)- Qt中的互斥鎖(QMutex和QMutexLocker)

互斥鎖管理類 說明
lock_guard 基於作用域的互斥量管理
unique_lock 更加靈活的互斥量管理
  • lock_guard , 沒有 lockunlock 僅提供構造時加鎖,析構時解鎖。
  • unique_lock , 允許延遲鎖定、鎖定的有時限嘗試、遞歸鎖定、所有權轉移和與條件變量一同使用。

比如可以這麼使用一個互斥鎖管理器:

void func(void)
{
	std::unique_lock<std::mutex> locker(g_mutex, std::defer_lock);
	locker.lock();

	++g_number;
}

locker 在構造的時候可以指定爲構造時加鎖還是不加鎖。
這裏使用 std::defer_lock 表明構造的時候不加鎖,使用 adopt_lock 表示構造的時候同時加鎖。
析構時,根據當前的鎖屬性判斷是否解鎖,如果當前爲加鎖狀態,析構時就解鎖。

unique_lock 它滿足可移動構造 (MoveConstructible) 和可移動賦值 (MoveAssignable) 但不滿足可複製構造 (CopyConstructible) 或可複製賦值 (CopyAssignable) 。
同時還有方法 swap (與另一 std::unique_lock 交換狀態) 和 release (將關聯互斥解關聯而不解鎖它)

關於 unique_lock 的更多使用,可參照:
https://zh.cppreference.com/w/cpp/thread/unique_lock


2. std::condition_variable

之前講了使用Qt創建和使用條件變量,C++11中也提供了對於條件變量的操作方法
線程的互斥和同步(7)- Qt的條件變量QWaitCondition

它同樣提供了wait和喚醒的方法

  • 等待:waitwait_forwait_until
  • 喚醒:notify_one (喚醒一個) 和 notify_all (喚醒全部)

這裏需要注意的是 虛假喚醒
虛假喚醒 是指可能一個線程沒有發送喚醒通知,但是該線程已經被喚醒。

通常防止虛假喚醒的做法是,使用一個 while 循環,如果是虛假喚醒,則繼續等待:

std::mutex g_mutex;
std::condition_variable var;
bool g_pred = false;

void func(void)
{
	std::unique_lock<std::mutex> locker(g_mutex);

	// 循環等待,防止虛假喚醒
	while (!g_pred)
	{
		// 當喚醒時,g_pred 將設置爲true
		var.wait(locker);
	}
}

也可以寫成如下方式:

std::mutex g_mutex;
std::condition_variable var;
bool g_pred = false;

void func(void)
{
	std::unique_lock<std::mutex> locker(g_mutex);

	// 防止虛假喚醒
	var.wait(locker, [] {return g_pred; });
}

此處的 wait 函數的第二個參數,爲一個仿函數(lambda表達式就是一個匿名仿函數),如果返回值爲 true 則忽略虛假喚醒;否則繼續等待。

關於 std::condition_variable 更多介紹,可參考:
https://zh.cppreference.com/w/cpp/thread/condition_variable


作者:douzhq
個人博客:不會飛的紙飛機
文章同步頁:線程的互斥和同步(8)- C++11中的互斥鎖和條件變量

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章