目錄
條件變量
頭文件
主要操作函數
1、等待操作
2、喚醒操作
使用示例
信號量
頭文件
主要操作函數
1、信號量初始化
2、等待操作(P操作)
3、信號操作(V操作)
4、獲取信號量值?
5、銷毀信號量
使用示例
互斥鎖
頭文件
使用示例
當我們需要給多個線程的指定執行順序的時候,我們通常有多種方法:
- 條件變量
- 信號量
- 互斥鎖
在這篇文章里,會介紹如何使用這三種方式來為多個線程指定執行順序,以及在使用的時候需要主義的地方。
條件變量
????????條件變量是C++11引入的同步原語,用于在多線程環境中實現線程間的等待和通知機制。它允許一個或多個線程等待某個條件成立,當條件滿足時,其他線程可以通知等待的線程繼續執行,一般需要配合unique_lock使用。
頭文件
#include <condition_variable>
主要操作函數
1、等待操作
a)基本形式
void wait(std::unique_lock<std::mutex>& lock);
b)帶謂詞形式
template<class Predicate>
void wait(std::unique_lock<std::mutex>& lock, Predicate pred);
?兩者的區別在于處理虛假喚醒的情況比較明顯,這個在后面介紹哈。
2、喚醒操作
a)喚醒單個線程
void notify_one() noexcept;
特點:喚醒等待隊列中的一個線程,具體是哪個線程是未定義的
b)喚醒所有線程
void notify_all() noexcept;
特點:喚醒等待隊列中的所有線程,性能開銷比較大,但是確保所有等待線程都被喚醒。
這里需要介紹一下虛假喚醒的問題
????????虛假喚醒是指線程在沒有收到notify_one或者notify_all調用的情況,從wait狀態中被喚醒。為什么會出現虛假喚醒的情況呢?因為可能會出現系統信號中斷條件變量的等待(SIGINT),或者因為底層I/O操作等底層系統調用中斷,導致pthread_cond_wait() 被中斷返回,因此出現虛假喚醒的情況。
? ? ? ? 在上面,我們介紹了兩種等待的方式,他們在處理虛假喚醒的情況表現有所不同。
? ? ? ? 帶謂詞的等待方式,會自動處理虛假喚醒,不需要我們再進行手動處理,那么他是怎么做到自動處理的呢,他的內部實現等價如下代碼,就是在循環中不斷判斷條件是否滿足,以此來處理虛假喚醒的情況。
// 帶謂詞的wait()函數的內部實現等價于:
template<class Predicate>
void wait(std::unique_lock<std::mutex>& lock, Predicate pred) {while (!pred()) { // 關鍵:自動循環檢查wait(lock); // 調用基本的wait()}// 退出循環時,保證 pred() 返回 true
}
? ? ? ? 基本的等待方式?需要我們手動處理虛假喚醒的情況,如下代碼是有問題的:
void wrong_basic_wait() {std::unique_lock<std::mutex> lock(mtx);// 錯誤:只等待一次,不處理虛假喚醒cv.wait(lock);// 假設條件一定滿足 - 危險!if (data_ready) {process_data();}
}
? ? ? ? 如果因為底層系統調用中斷了等待,但是此時條件并不滿足,比如數據并未準備好,會出現未定義的情況,因此,我們需要模仿帶謂詞的等待方式的等價寫法,在循環中判斷,如下:
void correct_basic_wait() {std::unique_lock<std::mutex> lock(mtx);// 正確:使用循環處理虛假喚醒while (!condition_satisfied()) {cv.wait(lock);// 如果是虛假喚醒,循環會繼續等待// 如果條件真的滿足,循環會退出}// 這里保證條件一定滿足process_data();
}
使用示例
1114. 按序打印 - 力扣(LeetCode)https://leetcode.cn/problems/print-in-order/description/
class Foo {condition_variable m_cv;mutex m_mtx;int m_nFlg;
public:Foo() {m_nFlg=1;}void first(function<void()> printFirst) {// printFirst() outputs "first". Do not change or remove this line.unique_lock<mutex> lock(m_mtx);m_cv.wait(lock,[=](){return m_nFlg==1;});printFirst();m_nFlg=2;m_cv.notify_all();}void second(function<void()> printSecond) {// printSecond() outputs "second". Do not change or remove this line.unique_lock<mutex> lock(m_mtx);m_cv.wait(lock,[=](){return m_nFlg==2;});printSecond();m_nFlg=3;m_cv.notify_all();}void third(function<void()> printThird) {// printThird() outputs "third". Do not change or remove this line.unique_lock<mutex> lock(m_mtx);m_cv.wait(lock,[=](){return m_nFlg==3;});printThird();m_nFlg=1;m_cv.notify_all();}
};
信號量
信號量的本質就是一個非負整數計數器,支持兩個原子操作:P(等待/減少)、V(信號/增加)
頭文件
#include <semaphore.h>
主要操作函數
1、信號量初始化
int sem_init(sem_t *sem, int pshared, unsigned int value);
參數說明:
- sem:指向信號量(sem_t)的指針
- pshared:0表示線程間共享,非0表示進程間共享
- value:信號量的初始值
返回值
- 返回0:初始化成功
- 返回-1:初始化失敗,同時設置errno的錯誤碼。
2、等待操作(P操作)
信號量等待有三種方式
a)sem_wait()-阻塞等待
int sem_wait(sem_t *sem);
特點:如果信號量值為0,線程會一直阻塞等待,知道信號量可用。
b)sem_trywait()-非阻塞等待
int sem_trywait(sem_t *sem);
特點:非阻塞等待,立即返回,不會等待,如果信號量不可用,立即返回-1,不會造成線程阻塞的情況,適用于輪詢場景
c)sem_timedwait() - 超時等待
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
特點:在指定時間內等待,超時后返回-1,使用絕對時間戳,不是相對時間。
這個參數比較多,這里演示下用法:
struct timespec結構體用于存儲超時時間:
- tv_sec:秒數
- tv_nsec:納秒數
#include <semaphore.h>
#include <iostream>
#include <time.h>
#include <errno.h>void timed_work() {struct timespec timeout;clock_gettime(CLOCK_REALTIME, &timeout);timeout.tv_sec += 5; // 5秒后超時int result = sem_timedwait(&sem, &timeout);if (result == 0) {std::cout << "在超時前獲取到信號量" << std::endl;// 執行臨界區代碼sem_post(&sem);} else {if (errno == ETIMEDOUT) {std::cout << "等待超時,放棄獲取" << std::endl;}}
}
?下面的代碼作用是獲取當前的系統時間,CLOCK_REALTIME表示使用系統實時時鐘
clock_gettime(CLOCK_REALTIME, &timeout);
3、信號操作(V操作)
釋放信號量,也就是將信號量的值+1。
int sem_post(sem_t *sem);
4、獲取信號量值?
int sem_getvalue(sem_t *sem, int *sval);
5、銷毀信號量
這種只能用于未命名的信號量,比如我們直接定義的sem_t sem,就屬于未命名信號量
int sem_destroy(sem_t *sem);
使用示例
1114. 按序打印 - 力扣(LeetCode)https://leetcode.cn/problems/print-in-order/description/這題希望我們指定三個線程的執行順序,我們可以定義三個信號量來進行控制
class Foo {sem_t s1,s2,s3;
public:Foo() {sem_init(&s1,0,1);sem_init(&s2,0,0);sem_init(&s3,0,0);}~Foo() {sem_destroy(&s1);sem_destroy(&s2);sem_destroy(&s3);}void first(function<void()> printFirst) {// printFirst() outputs "first". Do not change or remove this line.sem_wait(&s1);printFirst();sem_post(&s2);}void second(function<void()> printSecond) {// printSecond() outputs "second". Do not change or remove this line.sem_wait(&s2);printSecond();sem_post(&s3);}void third(function<void()> printThird) {// printThird() outputs "third". Do not change or remove this line.sem_wait(&s3);printThird();sem_post(&s1);}
};
互斥鎖
頭文件
#include <mutex>
使用示例
因為互斥鎖比較簡單這里,直接展示使用示例:1114. 按序打印 - 力扣(LeetCode)https://leetcode.cn/problems/print-in-order/
class Foo {mutex mtx1,mtx2,mtx3;
public:Foo() {mtx2.lock();mtx3.lock();}void first(function<void()> printFirst) {// printFirst() outputs "first". Do not change or remove this line.mtx1.lock();printFirst();mtx2.unlock();}void second(function<void()> printSecond) {// printSecond() outputs "second". Do not change or remove this line.mtx2.lock();printSecond();mtx3.unlock();}void third(function<void()> printThird) {// printThird() outputs "third". Do not change or remove this line.mtx3.lock();printThird();mtx1.unlock();}
};