一.線程執行體:
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。