C++多線程學習筆記一

一.線程執行體:

Lambda表達式的多線程

#include<iostream>
#include<thread>
#include<vector>
#include<algorithm>
using namespace std;
int main() {
    thread td([](int a, int b) {
        cout << a << "+" << b << "=" << a + b << endl;
    },1,2);
    td.join();
    system("pause");
}

對象的多線程

struct functor
{
    void operator()(int a, int b) {
        cout << a << "+" << b << "=" << a + b << endl;
    }
};
int main() {
    thread td(functor(),1,2);
    td.join();
    system("pause");
}

使用std::bind表達式綁定對象和其非靜態成員函數

using namespace std;
class C {
    int data_;
public:
    C(int data) :data_(data) {}
    void member_fun(int c) {
        cout << "this->data=" << this->data_ << "; extend c=" << c << endl;
    }
};
int main() {
    C obj(10);
    thread td(bind(&C::member_fun, &obj,3));
    td.join();
    system("pause");
}

使用Lambda表達式調用對象的非靜態成員函數

class C {
public:
    int data_;
    C(int data) :data_(data) {}
    void member_fun(int c) {
        cout << "this->data=" << this->data_ << "; extend c=" << c << endl;
    }
};
int main() {
    C obj(10);
    auto a = [obj]()mutable {obj.member_fun(3); };
    obj.data_ = 11;
    thread td(a);
    td.join();
    thread td2([&obj]() {obj.member_fun(4); });
    td2.join();
    system("pause");
}

注意結果的輸出,兩種lambda策略,上面一種是複製obj,下面是引用。所以打印時一個是10,一個是11

二.線程管理函數

1.

#include <iostream>  
#include <thread>  
#include <iomanip>  
  
int main()  
{  
  std::thread td([](){});  
  std::cout << "td.joinable() = " << std::boolalpha << td.joinable() << std::endl;  
  td.detach();  
  std::cout << "td.joinable() = " << std::boolalpha << td.joinable() << std::endl;  
}  

2.

#include <iostream>  
#include <thread>  
#include <iomanip>  
  
int main()  
{  
  std::thread td([](){});  
  std::cout << "td.joinable() = " << std::boolalpha << td.joinable() << std::endl;  
  td.join();  
  std::cout << "td.joinable() = " << std::boolalpha << td.joinable() << std::endl;  
}  

3.RAII

 class thread_guard {  
    std::thread& t_;  
  public:  
    explicit thread_guard(std::thread& t) : t_(t) { }  
    thread_guard(const thread_guard&) =delete;  
    thread_guard& operator=(const thread_guard&) =delete;  
  
    ~thread_guard() { if (t_.joinable()) t_.join(); }  
  };  

三.互斥Mutex

std::mutex 互斥對象

std::timed_mutex 帶有超時的互斥,超時後直接放棄

std::recusive_mutex 允許被同一個程序遞歸的lock unlock

std::recusive_timed_mutex 帶了超時的xx

std::shared_timed_mutex(c++14) 允許多個線程共享所有權的互斥對象,比如讀寫鎖

用mutex對set的insert操作進行保護,實現安全的併發訪問

#include<iostream>
#include<thread>
#include<vector>
#include<algorithm>
#include "ThreadGuard.h"
#include <set>
#include <mutex>
#include<random>
int main() {
	std::set<int> int_set;
	std::mutex mt;
	auto f = [&int_set, &mt]() {
		try {
			std::random_device rd;
			std::mt19937 gen(rd());
			std::uniform_int_distribution<> dis(1, 1000);
			for (std::size_t i = 0; i != 100000; ++i) {
				mt.lock();
				int_set.insert(dis(gen));
				mt.unlock();
			}
		}
		catch (...) {}
	};
	std::thread td1(f), td2(f);
	td1.join();
	td2.join();
	system("pause");
}

四.使用RAII管理互斥對象

std::lock_guard嚴格基於作用域的鎖管理類模板,構造時是否加鎖是可選的,析構時自動釋放鎖,所有權不可轉移,對象生存期內不允許手動加鎖和釋放鎖。。lock_guard 對象不可被拷貝構造或移動構造

std::unique_lock 更加靈活的鎖管理模板,構造時是否加鎖可選,在對象析構時如果持有鎖會自動釋放鎖,所有權可以轉移。對象生命週期允許手動加鎖和釋放鎖。構造(或移動(move)賦值)時,unique_lock 對象需要傳遞一個 Mutex 對象作爲它的參數,新創建的 unique_lock 對象負責傳入的 Mutex 對象的上鎖和解鎖操作。

unique_lock(const unique_lock&) = delete;
unique_lock(unique_lock&& x);

 

 

std::shared——lock(c++14)

#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex, std::lock_guard, std::adopt_lock
#include <chrono>
#include <stdexcept>

std::mutex mtx;           // mutex for critical section

void print_thread_id(int id) {
	try {
		for (int i = 0; i < 10; i++) {
			//mtx.lock();
			//std::lock_guard<std::mutex> lck(mtx, std::adopt_lock
			//std::lock_guard<std::mutex> lck(mtx);
			std::this_thread::sleep_for(std::chrono::milliseconds(1));
			std::cout << "thread #" << id << ">>" << i <<'\n';
			if (i == 7) throw (std::logic_error("fake error"));
			//mtx.unlock();
		}
	}
	catch (...) {
		std::cout << "exception" << std::endl;
	}
}

int main()
{
	std::thread threads[10];
	// spawn 10 threads:
	for (int i = 0; i<10; ++i)
		threads[i] = std::thread(print_thread_id, i + 1);
	for (auto& th : threads) th.join();
	system("pause");
	return 0;
}

加鎖策略:

1.默認 請求鎖,阻塞當前線程直到成功獲得鎖  三種都支撐

2.std::defer_lock 不請求鎖                                 unique_lock,shared_lock

3.std::try_to_lock 嘗試請求鎖,但不阻塞線程,鎖不可用時也會立即返回     unique_lock,shared_lock

4.std::adopt_lock 假定當前線程已經獲得互斥對象的所有權,所以不再請求鎖   lock_guard,unique_lock,shared_lock

{
    std::unique_lock<std::mutex> lock1(mutex1, std::defer_lock);
    std::unique_lock<std::mutex> lock2(mutex2, std::defer_lock);
    std::lock(mtx1, mtx2);
    do_sth();
}


{
    std::unique_lock<std::mutex> lock1(mutex1, std::try_to_lock);
    if(lock1.owns_lock()){
        do_sth1();
    } esle {
        do_sth2();
    }
}

std::unique_lock std::lock_guard都能實現自動加鎖與解鎖功能,但是std::unique_lock提供了 lock(), unlock() try_lock() 函數,要比std::lock_guard靈活控制鎖的範圍,減小鎖的粒度但是更靈活的代價是佔用空間相對更大一點且相對更慢一點

五.條件變量

條件變量:一種同步原語(Synchronization Primitive)用於多線程之間的通信,它可以阻塞一個或同時阻塞多個線程直到,收到來至其他線程的通知;超時;發送虛假喚醒(Spurious Wakeup)。

C++11的條件變量有兩個類

std::condition_variable:必須與std::unique_lock配合使用

std::condition_variable_any:更加通用的條件變量,可以與任意類型的鎖配合使用,相比前者使用時會有額外的開銷

兩者在線程要等待條件變量前,都必須要獲取相應的鎖

二者相同的成員函數:

notify_one

notify_all

wait

wait_for  >>超時設置爲時間長度

wait_until >>超時設置爲時間點

遺留說明:

condition_variable_any的額外開銷是什麼?虛假喚醒是啥?

#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex, std::lock_guard, std::adopt_lock
#include <chrono>
#include <stdexcept>
#include <queue>
#include <string>
#include <ctime>
#include <sys/timeb.h>
std::mutex mtx;           // mutex for critical section
std::queue<std::string> dataQueue;
std::condition_variable dataCond;
bool isStop = false;


std::string getSystemTime()
{
	std::this_thread::sleep_for(std::chrono::milliseconds(10));
	struct timeb t;
	ftime(&t);
	return std::to_string(1000 * t.time + t.millitm);
}


void DataProduceThread() {
	while (true) {
		std::string data = getSystemTime();
		std::lock_guard<std::mutex> lock(mtx);
		dataQueue.push(data);
		dataCond.notify_one();//嘗試註釋該行,執行下,有助於理解條件變量有什麼用
		if (isStop) break;
	}
}

void DataConsumeThread(int consumerId) {
	while (true)
	{
		std::unique_lock<std::mutex> lock(mtx);
		dataCond.wait(lock, [] {return !dataQueue.empty(); });
		std::string data = dataQueue.front();
		dataQueue.pop();
		lock.unlock();//wait返回時mutex處於locked狀態,爲了提高併發應用效率,應立即顯示解鎖,後繼續處理數據
		//handle data....
		std::cout << "[consumerId:" << consumerId << "] handle data: " << data << std::endl;
		std::this_thread::sleep_for(std::chrono::milliseconds(100));
		if (isStop) break;
	}
}



int main()
{
	std::thread pd(DataProduceThread);
	std::thread consumers[10];
	for (int i = 0; i < 10; i++) {
		consumers[i] = std::thread(DataConsumeThread, i + 1);
	}
	std::this_thread::sleep_for(std::chrono::seconds(10));
	isStop = true;
	pd.join();
	for (auto& t : consumers) t.join();
	system("pause");
	return 0;
}

結合這個例子,大家可以再試着將consumer減到3,在lock獲取,cond通過後的地方加上打印,結合具體大家,個人得出下面心得:

1.雖然打印會有亂序,原因是lock.unlock後才進行的數據處理。導致cout輸出流程,存在併發衝突調用的情況。如果將lock.unlock移到線程等待的sleep_for前面,就不會有這個問題了。但如果移到sleep_for後面程序會沒法跑結束,爲什麼呢?

這個是因爲,dataCond.wait(lock,[]{})。條件變量的wait是阻塞等待,當produce線程先停止後,經notify_one(),導致總會有線程沒有被喚醒,出現阻塞卡死等待。

這時候可以這樣驗證下,將producer裏的notify_one()改成notify_all()。恩,結果發現還不行?!!

那個這個時候就要在看下dataCond.wait(lock,[]{})這個了。可以看到wait有兩個參數,後面那個是lambda表達式,啥式不是關鍵。關鍵是這個參數的作用,簡單看下定義,它是條件判斷。換句話說,condition被喚醒了還不算真被喚醒,他還可以通過這個參數進行判斷,到底是否滿足條件,如果不行它還是會進行阻塞,等待下一次喚醒。

所以如果要驗證,可以這樣再改下,把判斷條件去了dataCond.wait(lock);然後就可以跑完,正常退出了。

最後這個問題到底應該怎麼正確修復了,個人覺得可以將wait改爲wait_for,超時則退出,進行下一次循環,不要一直死等。當然判斷條件函數還是有意義的,可以防止虛假喚醒,提高整體的運作效率。有興趣的同學,可以想辦法構造驗證下如果producer是notify_one,加入第一個被notify的線程A不滿足條件沒被喚醒,是否會有其他的線程B繼續被notify然後判斷是否滿足條件。恩這個個人感覺是這樣的,但畢竟沒實際驗證,可能有出入。dataCond.wait_for(lock, std::chrono::milliseconds(2), [] {return !dataQueue.empty(); });

 

2.通過打壓分析,多線程之間,mtx是生效的,lock鎖可以保障同時只有一個可以進入,算是實現了併發衝突的解決。

3.既然mtx+lock已經實現了併發衝突,condition的意義到底是什麼?我如今的理解是阻塞,防止線程無意義的空轉。條件變量,不滿足條件就別xx嘛。mtx是互斥鎖,本質上他的作用是解決併發衝突的;lock只是對mutex的封裝,本質上解決lock和unlock分開寫,過程異常導致未正常釋放的問題;所以這麼看mutex和lock都是沒有阻塞等待的作用。so如果不想空轉,又不想沒輪等個xx時間再來一輪,就有了condition。

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