【C++多線程系列】【八】線程間通訊之條件變量

有以下場景:線程A須要等線程B完成操做後,再進行執行;在線程B完成操做前,線程A出於睡眠狀態,當線程B完成操做後,喚醒線程A。這裏,注意,在線程B完成前,A是處於睡眠狀態,即此時,A不佔用CPU,不可用原子變量+while死循環來等待(while(!done)),這樣的話CPU會處於一直運行狀態。ios

可使用sleep來處理,但因爲A不知道須要sleep多長時間,因此,sleep不合適。這裏使用條件變量,來處理該問題。函數

條件變量:當收到notify通知時,檢查是否知足某個條件,知足時,線程中止wait,被喚醒,從新加鎖,繼續執行。this

總體流程以下圖,具體見代碼註釋atom

#include<iostream>
#include<thread>
#include<mutex>
#include<condition_variable>
using namespace std;

// 使用條件變量的三劍客:
/*
1.互斥元
2.條件變量
3.條件值done
*/
mutex mA;
condition_variable cv;
bool done = false;

void f()
{
	unique_lock<mutex> _1(mA);
	// 條件變量的wait所必須是unique_lock而不是lock_guard,由於wait會在內部調用unique_lock.unlock先解鎖,當被喚醒後,條件知足時,會unique_lock.lock
	// 條件爲:當done爲true時,收到notify的線程會被喚醒,不然即便收到notify,也不會被喚醒
	cv.wait(_1, [] {return done; });
	cout << "has done" << endl;

	// 須要手動釋放鎖
	_1.unlock();
}

void f2()
{
	// 這裏使用lock_guard在mA上加鎖便可
	lock_guard<mutex> _1(mA);
	cout << "f2" << endl;
	std::this_thread::sleep_for(1s);

	//必須將條件done設置爲true,不然線程t1不會被喚醒
	done = true;

	//通知一個線程,讓收到的線程檢查其條件,收到通知的線程發現條件知足,則該線程會被喚醒
	cv.notify_one();
}


int main(int argc, int * argv[])
{
	thread t1(f);

	thread t2(f2);

	t1.join();
	t2.join();

	cout << "main" << endl;
	system("pause");
}

結果以下:spa

 

注意,若是在f2中,忘記將done 設置爲 true,則f1不會被喚醒,會一直等待:線程

void f2()
{
	// 這裏使用lock_guard在mA上加鎖便可
	lock_guard<mutex> _1(mA);
	cout << "f2" << endl;
	std::this_thread::sleep_for(1s);

	//必須將條件done設置爲true,不然線程t1不會被喚醒
	//done = true;

	//通知一個線程,讓收到的線程檢查其條件,收到通知的線程發現條件知足,則該線程會被喚醒
	cv.notify_one();
}

結果以下:code

 

因此:條件變量的本質在於,只有當條件被知足時,線程纔會被喚醒,而不是收到notify了,該等待線程就會被喚醒。源碼

 

條件變量實際上實現了線程間的數據共享操做。線程A在修改完某些數據後,經過條件變量,來通知線程B來獲取最新的數據修改值。it

代碼能夠以下:io

#include<iostream>
#include<thread>
#include<mutex>
#include<condition_variable>
using namespace std;

// 使用條件變量的三劍客:
/*
1.互斥元
2.條件變量
3.條件值done
*/
mutex mA;
condition_variable cv;
bool done = false;

int age = 0;

void f()
{
	unique_lock<mutex> _1(mA);
	// 條件變量的wait所必須是unique_lock而不是lock_guard,由於wait會在內部調用unique_lock.unlock先解鎖,當被喚醒後,條件知足時,會unique_lock.lock
	// 條件爲:當done爲true時,收到notify的線程會被喚醒,不然即便收到notify,也不會被喚醒
	cv.wait(_1, [] {return done; });
	cout << "has done" << endl;
	cout << "age=" << age << endl;
	// 須要手動釋放鎖
	_1.unlock();
}

void f2()
{
	// 這裏使用lock_guard在mA上加鎖便可
	lock_guard<mutex> _1(mA);
	cout << "f2" << endl;
	std::this_thread::sleep_for(1s);

	age = 1000;
	//必須將條件done設置爲true,不然線程t1不會被喚醒
	done = true;

	//通知一個線程,讓收到的線程檢查其條件,收到通知的線程發現條件知足,則該線程會被喚醒
	cv.notify_one();
}


int main(int argc, int * argv[])
{
	thread t1(f);

	thread t2(f2);

	t1.join();
	t2.join();

	cout << "main" << endl;
	system("pause");
}

age做爲兩個線程的共享變量,一個修改,一個使用修改後的值。

結果以下:

 

【坑】

在利用條件變量進行線程間等待與通知時,wait能夠不用傳遞謂詞,即判斷條件函數。

這時,會有一個問題。若是線程A先notify,而線程B後wait,則線程B永遠不會被喚醒。

#include<iostream>
#include<thread>
#include<mutex>
#include<condition_variable>
#include<future>
#include<chrono>
using namespace std;


mutex mA;
condition_variable cv;

void f()
{
	lock_guard<mutex> lock(mA);
	std::this_thread::sleep_for(1s);
	cv.notify_one();
	cout << "notify" << endl;
}

void f2()
{
	unique_lock<mutex> lock(mA);
	cout << "wait" << endl;
    // wait沒有傳入第二個參數
	cv.wait(lock);
	cout << "wake" << endl;
	lock.unlock();
}


int main(int argc, int * argv[])
{


	thread t1(f);  // 先notify
	thread t2(f2); // 後wait,一直等待,不會被喚醒

	t1.join();
	t2.join();

	cout << "main" << endl;
	system("pause");
}

結果以下:一直在wait,喚醒失敗。

其實,這是一個先通知後等待的問題,若是先等待,後通知,則沒有問題。

#include<iostream>
#include<thread>
#include<mutex>
#include<condition_variable>
#include<future>
#include<chrono>
using namespace std;


mutex mA;
condition_variable cv;

void f()
{
	lock_guard<mutex> lock(mA);
	std::this_thread::sleep_for(1s);
	cv.notify_one();
	cout << "notify" << endl;
}

void f2()
{
	unique_lock<mutex> lock(mA);
	cout << "wait" << endl;
	cv.wait(lock);
	cout << "wake" << endl;
	lock.unlock();
}


int main(int argc, int * argv[])
{


	
	thread t2(f2); // 先wait,
	thread t1(f);  // 後notify
	t1.join();
	t2.join();

	cout << "main" << endl;
	system("pause");
}

結果以下:線程正常被喚醒

 

如何解決這個問題呢?

方法一:給wait條件第二個參數,即謂詞條件。

方法二:不給wait添加第二個參數,可是使用done標誌位。當notify以後,該標誌位設置爲true。wait以前先判斷該標誌位。若是先通知,則該標誌位爲true,不會調用cv.wait,也就不會進入等待

#include<iostream>
#include<thread>
#include<mutex>
#include<condition_variable>
#include<future>
#include<chrono>
#include<atomic>
using namespace std;


mutex mA;
condition_variable cv;
atomic<bool> done(false);
void f()
{
	lock_guard<mutex> lock(mA);
	std::this_thread::sleep_for(1s);
	cv.notify_one();
	done = true;
	cout << "notify" << endl;
}

void f2()
{
	unique_lock<mutex> lock(mA);
	
	while (!done) {
		cout << "wait" << endl;
		cv.wait(lock);
	}
	
	cout << "wake" << endl;
	lock.unlock();
}


int main(int argc, int * argv[])
{


	thread t1(f);  // 先notify
	thread t2(f2); // 後wait,
	
	t1.join();
	t2.join();

	cout << "main" << endl;
	system("pause");
}

結果以下:

 

其實,從本質上來講,方法一與方法二是相同的,看看vs2017裏面的源碼

 

因此,爲了不這種喚醒是失敗的問題(失敗的緣由在於先notify後wait),最好使用第一種方法,這種作法簡單,方便。邏輯上比第二種作法清晰。

相關文章
相關標籤/搜索