需求 任務隊列中可以依次添加任務; 任務執行函數需要接受外部傳輸的參數; 主動調用Start開始執行任務; 代碼實現 class TaskQueue { private: std::mutex mtx; std::condition_variable cv; std::queue<std::func ...
需求
- 任務隊列中可以依次添加任務;
- 任務執行函數需要接受外部傳輸的參數;
- 主動調用Start開始執行任務;
代碼實現
class TaskQueue {
private:
std::mutex mtx;
std::condition_variable cv;
std::queue<std::function<void()>> task_queue;
std::atomic<bool> is_running;
public:
TaskQueue() : is_running(false) {}
~TaskQueue() {}
// std::forward is used to forward the parameter to the function
template<typename F, typename... Args>
void Push(F&& f, Args&&... args) {
std::lock_guard<std::mutex> lock(mtx);
task_queue.push(std::bind(std::forward<F>(f), std::forward<Args>(args)...));
cv.notify_one();
}
void Start() {
is_running = true;
std::thread t([this] {
while(is_running) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [this] { return !task_queue.empty(); });
auto task = task_queue.front();
task_queue.pop();
lock.unlock();
task();
}
});
t.detach();
}
void Stop() {
is_running = false;
}
};
int main(int argc, char** argv) {
TaskQueue tq;
tq.Push(DoSomething, 1);
tq.Push(DoSomething, 2);
tq.Push(DoSomething, 3);
tq.Start();
tq.Push(DoSomething, 4);
// 等待任務結束
while(1) {
std::this_thread::sleep_for(std::chrono::seconds(1));
}
return 0;
}
實現筆記
任務隊列,將需要執行的任務存儲在隊列中,存儲的這個動作類似於生產者;
當任務隊列不為空時,會從隊列中取出一個任務執行,當任務執行結束後再從隊列取下一個,直到隊列為空;
執行任務類似於消費者;
基礎概念理解
- C++左值和右值
判斷表達式左值還是右值的兩種辦法:
a. 位於賦值符號=
左側的就是左值,只能位於右側的就是右值;需要註意的是,左值也可以當右值用;
b. 有名稱、可以取到存儲地址的表達式就是左值,否則就是右值;
C++右值引用(用 &&
標識)
1. 和左值引用一樣,右值引用也需要立即被初始化,且只能使用右值進行初始化
int num = 10;
// 左值不能用於初始化右值
// int &&a = num; 編譯報錯
int &&a = 123;
2. 和常量左值引用不同的是,右值引用可以對右值進行修改:
int num = 10;
int &&ref = 12;
ref = 222;// 修改右值引用的值
std::cout << ref << std::endl;
std::unique_lock
std::unique_lock
是個類模板,工作中,一般使用std::lock_guard
(推薦使用) ,std::unique_lock
比std::lock_guard
靈活很多,效率上差一點,記憶體占用多一點。
- std::async 和 std::future
std::async
是個函數模板,用來啟動一個非同步任務,啟動起來一個非同步任務之後(什麼叫“啟動一個非同步任務”,就是自動創建一個線程並開始執行對應的線程入口函數),他返回一個std::future
對象,這個std::future
對象裡面就含有線程函數返回的結果,我們可以通過調用std::future
對象的成員函數get()
來獲取結果;它返回一個std::future
對象。
- 條件變數
std::condition_variable
std:: condition_variable
實際上是個類,是一個與條件相關的類,說白了就是等待一個條件的達成。這個類是需要和互斥量來配合工作的,用的時候我們要生成這個類的對象。
a. wait()
:
1. 若第二個參數是true,wait()直接返回;
2. 若第二個參數是Lambda表達式,且**返回值是false,wait()將解鎖互斥量,且在本行阻塞**。阻塞到何時結束呢?堵塞到其他線程調用notify_one() 為止;
3. 若wait沒有第二個參數,則預設false;
b. notify_one()
和wait()
的工作流程:
其他線程用notify_one()將本wait(原本是睡著/堵塞)的狀態喚醒後,wait就開始恢復幹活了,恢復後的wait乾什麼活?
1. wait不斷地嘗試重新獲取互斥量鎖,如果獲取不到,那麼流程就卡在wait這裡等著獲取,如果獲取到,那麼wait就繼續執行b
2. 上鎖(實際上獲取到了鎖,等同於上鎖);
3. 若wait有第二個參數,就判斷lambda的表達式值,若值為false,則wait又對互斥量解鎖,休眠;直到lambda值為true時,才會執行下一步;
4. 為防止假喚醒,wait()中要有第二個參數(lambda)並且這個lambda中要正確處理公共數據是否存在;
完美轉發
定義:
函數模板可以將自己的參數“完美”地轉發給內部調用的其它函數。所謂完美,即不僅能準確地轉發參數的值,還能保證被轉發參數的左、右值屬性不變。
即不管傳入的參數是什麼,都能夠很好的匹配函數需要的參數類型;
C++11實現:
#include <iostream>
using namespace std;
// 接收左值
void ref_func(int& t) {
cout << "lvalue\n";
}
void ref_func(const int& t) {
cout << "rvalue\n";
}
//實現完美轉發的函數模板
template <typename T>
void function(T&& t) {
ref_func(forward<T>(t));
}
int main()
{
function(5); // rvalue
int x = 1;
function(x); // lvalue
return 0;
}
代碼中,重載的函數ref_func
可以接收一個左值引用,也可以接收一個右值引用,但這需要定義兩個函數進行重載。為了實現形式的統一,定義了一個模板函數function
,函數體內調用ref_func
函數,該模板函數接收參數後,會將參數類型轉到具體的函數中進行調用。
完美轉發需要考慮的一些問題:
- C++11規定,通常情況下右值引用形式的參數只能接收右值,不能接收左值;
- 對於函數模板中的使用右值引用語法定義的參數來說,上述規定不再有效。模板函數的右值引用參數既可以接收左值引用,也可以接收右值引用。此時的右值引用也被稱為萬能引用。
- 在實現完美轉發的時候,只要函數模板的參數類型為T&&,C++就可以自行準確判定實際傳入的實參是左值還是右值;
- 如何將函數模板接收到的形參,連同參數的左右值屬性,一切傳遞給被調用的函數呢?
- C++11為瞭解決這個問題,引入了std::forward()模板
//實現完美轉發的函數模板
template <typename T>
void function(T&& t) {
// 將形參和其左右值屬性傳遞給被調用的函數
ref_func(std::forward<T>(t));
}
隊列實現
- 添加任務的實現
- 需要將不同任務添加進隊列中,函數名可能不一樣,參數也不一樣
- 要求能夠添加不同的函數,執行不同的任務;
實現原理:
a. 類內定義一個隊列,元素是std::function<void()>
,即std::function
對象;
b. 使用一個模板函數,和完美轉發特性,將不同的函數添加進隊列中;
template<typename F, typename... Args>
void Push(F&& f, Args&&... args) {
std::lock_guard<std::mutex> lock(mtx);
task_queue.push(std::bind(std::forward<F>(f), std::forward<Args>(args)...));
cv.notify_one();
}
在
Push
函數中使用了std::bind
類模板,將傳入函數f和其需要的參數綁定在一起,生成一個std::function
類對象,
往隊列中添加完任務之後,則需要通過條件變數cv通知消費者可以進行消費。
- 按序執行任務,需要從隊列中一個個取出來執行,
void Start() {
is_running = true;
std::thread t([this] {
while(is_running) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [this] { return !task_queue.empty(); });
auto task = task_queue.front();
task_queue.pop();
lock.unlock();
task();
}
});
t.detach();
}
這裡將創建的執行任務線程用detach方法放在後臺執行,
這裡將創建的執行任務線程用detach
方法放在後臺執行,當隊列中沒有任務可以執行的之後,將會等待隊列中有任務時在執行,將一直阻塞在cv.wait(lock, [this] { return !task_queue.empty(); });
中。
使用說明
-
先生成一個任務隊列的對象;
-
調用
Push
將需要執行的函數和參數加到隊列中; -
調用
Start
介面,讓任務按序執行;
拓展:
- 如果要等任務結束後在執行下一個任務,則需要在task()後面加上一個條件變數,等待任務結束在取下一個任務;
- 若要讓執行任務的線程一開始就運行,則可以將Start函數放在構造函數中;