關於C++11標準中的condition_variable對象使用

可能C++11標準似乎也準備在過時了。
最近研究了一下C++11的並行化,其實thread包裝的還是不錯的,鎖的功能也比較豐富,也許是我沒什麼多線程的經驗纔會這麼認爲吧。

但是C++11官方對條件變量的實現機制,說的很不明確,讓人看得雲裏霧裏,同時例子也很詭異。
天下文章一大抄,有很多網上的介紹也沒有寫出自己的想法來,都是翻譯的官方的說法。
這個文章也不例外,僅僅做一下自己學習的思路記錄和想法。

具體見:http://www.cplusplus.com/reference/condition_variable/condition_variable/

例子如下

// condition_variable example
#include <iostream>           // std::cout
#include <thread>             // std::thread
#include <mutex>              // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void print_id (int id) {
  std::unique_lock<std::mutex> lck(mtx);
  while (!ready) cv.wait(lck);
  // ...
  std::cout << "thread " << id << '\n';
}

void go() {
  std::unique_lock<std::mutex> lck(mtx);
  ready = true;
  cv.notify_all();
}

int main ()
{
  std::thread threads[10];
  // spawn 10 threads:
  for (int i=0; i<10; ++i)
    threads[i] = std::thread(print_id,i);

  std::cout << "10 threads ready to race...\n";
  go();                       // go!

  for (auto& th : threads) th.join();

  return 0;
}

說明爲

condition variable is an object able to block the calling thread until notified to resume.
It uses a unique_lock (over a mutex) to lock the thread when one of its wait functions is called. The thread remains blocked until woken up by another thread that calls a notification function on the same condition_variable object.
Objects of type condition_variable always use unique_lock<mutex> to wait: for an alternative that works with any kind of lockable type, see condition_variable_any

條件變量對象是用來阻止線程而調用的,直到通知恢復它(這個通知是可以從其他線程發出)。
當它調用wait函數的時候,用一個unique_lock對象來鎖住線程,線程將會一直被阻塞,直到其他線程調用同一個條件變量對象的notification函數,來解開本線程的阻塞狀態。
條件變量對象總是用一個unique_lock對象來等待,對於調用其他鎖類型的方法,可以參見condition_variable_any。

個人理解

這段話大概講清楚了condition_variable的作用,上面例程也體現了這一點,但是代碼中有一點就看不懂了,不是說unique_lock帶互斥量的構造函數會自動調用互斥量的lock方法麼,爲啥它main一上來就開了10個線程,最後還想表達一下10個線程互相比賽的意思??
既然一個線程獲得了鎖,其他線程進行不下去纔對啊,那go函數還能運行下去?這不是死鎖了??

最後找到了這個比較好的資料,詳細介紹了C++11多線程中實現的進一步淺層的機制
https://baptiste-wicht.com/posts/2012/04/c11-concurrency-tutorial-advanced-locking-and-condition-variables.html
A condition variable manages a list of threads waiting until another thread notify them. Each thread that wants to wait on the condition variable has to acquire a lock first. The lock is then released when the thread starts to wait on the condition and the lock is acquired again when the thread is awakened.
條件變量管理等待另一個線程通知它們的線程列表。想要在條件變量上等待的每個線程必須首先獲取鎖。當線程進入條件變量wait函數的時候,釋放鎖,並且當線程被喚醒時再次獲取鎖定。

最後代碼例子可以理解爲

首先建立了10個線程,然後每個線程都先聲明自己有這個鎖的權力,也就是unique_lock構造函數,之後調用wait方法,釋放了對鎖的所有權,也就是鎖沒了,等待着獲得鎖,整個線程將阻塞。
之後進入go函數,先獲得鎖,然後標誌位轉換,最後調用條件變量的notify_all方法,函數執行完,自動釋放鎖,像大人逗孩子一樣把鎖拋了出去,留下的是10個互相爭搶鎖的孩子。。所以纔是可以理解爲,10個線程互相搶鎖的過程。也是讓所有線程同步的一種方法。

遺留的問題:

那麼,那個ready到底起了什麼作用呢,或者這個while起了什麼作用呢?
我們現在把ready的影響去掉,代碼改爲:

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <chrono>

std::mutex mtx;	//全局互斥鎖
std::condition_variable cv;//全局條件變量

void do_print_id(int id)
{
	std::unique_lock<std::mutex> lck(mtx);		//獲得鎖
	cv.wait(lck);
	std::cout << "thread" << id << "\n";
}

void go()
{
	std::unique_lock<std::mutex> lck(mtx);
	cv.notify_all();
}

int main()
{
	std::thread threads[10];

	for (int i = 0; i < 10; i++)
	{
		threads[i] = std::thread(do_print_id, i);
	}
	std::cout << "10 threads ready to race...\n";
	go();
	for (auto& th : threads)
		th.join();

	getchar();
	return 0;
}

線程執行的效果是一樣的,沒有錯誤。

我們可以在c++11的參考中找到答案:
http://www.cplusplus.com/reference/condition_variable/condition_variable/wait/
wait實現了線程的阻塞,不是通過線程鎖的鎖定而實現的,是通過線程鎖的釋放而實現的,或者換句話說,wait造成的阻塞和輸入線程鎖一點物理關係都沒有,wait函數先丟棄鎖,然後再阻塞線程,當外界調用喚醒函數notify_one/all的時候,線程解除阻塞,再獲得鎖的控制。但是這個時候,有可能鎖是被別的線程拿着的,喚醒線程取不到鎖控制。
這個時候就造成了我明明通知線程喚醒,但是線程卻由於鎖沒有拿到而接着阻塞在那裏,形成假喚醒(spurious wake-up calls)。
爲了避免這個情況,提供了這個while,wait函數僅在ready爲false的情況下,纔會導致線程因爲wait而阻塞,並且notifications只能在ready爲true的情況下解除阻塞,如果ready不爲true,那麼當notification導致線程開始運轉的時候,會接着在while裏循環,再次進入等待notifation的狀態。這個ready作爲一個是否可以激活線程的一個總閘的作用,是個狀態量,而notification是一個觸發量。這對於檢查假喚醒是十分有用的一個手段。
這也是wait的第二種調用方法,輸入爲一個已經獲得鎖和一個返回爲bool量的函數(更廣義點 可調用的返回爲bool的對象)。

 

一個小小的鎖都能有深刻和複雜的理解,也可以看見,深刻理解C++11對頭髮是有多不好了。。。
 

 

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