// 提交任務到線程池
template<class F, class... Args>
auto ThreadPool::enqueue(F&& f, Args&&... args) -> std::future<typename std::result_of<F(Args...)>::type> {using return_type = typename std::result_of<F(Args...)>::type;auto task = std::make_shared<std::packaged_task<return_type()>>(std::bind(std::forward<F>(f), std::forward<Args>(args)...));std::future<return_type> res = task->get_future();{std::unique_lock<std::mutex> lock(queue_mutex);if(stop) {throw std::runtime_error("enqueue on stopped ThreadPool");}tasks.emplace([task]() { (*task)(); });}condition.notify_one();return res;
}
基礎知識點包括:右值、右值引用、完美轉發、智能指針、Lambda表達式、可調用對象包裝器function<void()>、綁定器bind、future、packaged_task
問題1:函數名(F&& f, Args&&… args) 這里的形參是啥意思?
在 ThreadPool::enqueue
這個函數的入參部分:
template <class F, class... Args>
auto ThreadPool::enqueue(F&& f, Args&&... args) -> std::future<typename std::result_of<F(Args...)>::type>
F&& f, Args&&... args
采用了一種 完美轉發(Perfect Forwarding) 機制,主要是為了高效地傳遞參數,并保持參數的值類別(左值/右值)屬性。
1. F&& f
- 這里
F&&
是一個 通用引用(Universal Reference),用于接收任何類型的可調用對象(callable object),包括:- 函數指針
- Lambda 表達式
std::function
- 具有
operator()
的仿函數(functor) - 綁定的成員函數
std::bind
F&&
作為通用引用,不僅能接受左值(Lvalue),還能接受右值(Rvalue),并且能保留參數的原始類別。
2. Args&&... args
Args...
是 可變模板參數(variadic template parameter),表示可以接受多個參數。Args&&...
也是通用引用,它的作用是:- 可以接受任意數量的參數
- 完美轉發(Perfect Forwarding):如果
args
是左值,則保持左值;如果args
是右值,則保持右值
3. 為什么要這樣寫?
主要是為了 提高泛型代碼的效率,并 避免不必要的拷貝。
如果直接使用:
template <class F, class... Args>
auto ThreadPool::enqueue(F f, Args... args) -> std::future<typename std::result_of<F(Args...)>::type>
那么:
F f
會導致可調用對象(如 lambda)被拷貝,而不是以原始形式傳遞。Args... args
會導致所有參數都被拷貝或切片(slicing),而不是以正確的引用形式傳遞。
使用 F&& f, Args&&... args
的 完美轉發,可以:
- 傳遞左值時,保留左值屬性,避免不必要的拷貝。
- 傳遞右值時,移動而不是拷貝,提高效率。
- 允許傳遞任意類型的參數,使
enqueue
適用于各種可調用對象。
問題2:理解 std::result_of<F(Args…)>::type
std::result_of<F(Args...)>::type
是 C++11/14 中用于推導可調用對象 F
以 Args...
作為參數調用后的返回類型的工具。
然而,這個特性在 C++17 被棄用,在 C++20 被移除,并由 std::invoke_result_t<F, Args...>
取代。
1?? 基本概念
std::result_of<F(Args...)>::type
解析 F
作為函數、函數指針、lambda 表達式或可調用對象,在傳入 Args...
參數后,它的返回值是什么。
通俗解釋:
- 如果
F(Args...)
是一個可調用表達式,那么std::result_of<F(Args...)>::type
就是它的返回值類型。
問題3:make_shared<std::packaged_task<return_type()>>(std::bind(std::forward(f), std::forward(args)…))如何理解?
解析 std::make_shared
在 std::packaged_task
中的用法
auto task = std::make_shared<std::packaged_task<return_type()>>(std::bind(std::forward<F>(f), std::forward<Args>(args)...)
);
1. 代碼結構拆解
std::make_shared<T>(...)
:<>
里是 要創建的對象類型:這里是std::packaged_task<return_type()>
()
里是 構造函數參數:這里是std::bind(...)
的返回值
2. std::packaged_task
介紹
std::packaged_task
是 C++11 引入的 任務封裝類,用于異步任務執行。它封裝一個可調用對象(函數、lambda 表達式等),并允許獲取其執行結果。
std::packaged_task<return_type()> task(func);
- 作用:將
func
綁定到task
,稍后可以執行task()
來調用func
,并通過std::future
獲取結果。 - 適用場景:
- 異步任務執行(如
std::thread
、std::async
) - 任務隊列(線程池)
- 異步任務執行(如
3. std::bind
介紹
std::bind
用于綁定函數和參數,返回一個可調用對象。
std::bind(f, args...)
- 作用:
- 預綁定
f
的參數args...
- 返回一個可調用對象(類似
lambda
) - 適用于回調函數和延遲執行
- 預綁定
示例
#include <iostream>
#include <functional>int add(int a, int b) { return a + b; }int main() {auto bound_func = std::bind(add, 10, 20);std::cout << bound_func() << std::endl; // 輸出 30
}
4. 為什么要用 std::forward?
在 enqueue
函數中使用 std::forward
的主要目的是實現完美轉發(Perfect Forwarding),確保參數 f
和 args...
能夠保持它們原本的值類別(左值或右值),從而避免不必要的拷貝或移動,提高程序的性能。
詳細解析:
1. F&& f
和 Args&&... args
是 萬能引用(Universal References)
F&&
和Args&&...
并不是普通的右值引用,而是模板參數推導下的萬能引用。- 當
f
和args...
被傳入時,它們可能是左值(lvalue
)或者右值(rvalue
)。 - 如果直接傳遞這些參數,不加
std::forward
,可能會導致不必要的拷貝或移動,影響性能。
2. 為什么需要 std::forward
?
-
在
std::bind(std::forward<F>(f), std::forward<Args>(args)...)
這部分代碼中:std::forward<F>(f)
確保f
被正確地轉發:- 如果
f
是左值,則std::forward<F>(f)
也是左值。 - 如果
f
是右值,則std::forward<F>(f)
也是右值(即std::move(f)
)。
- 如果
std::forward<Args>(args)...
也是同理,保證每個參數args...
都按照它原本的類別傳遞。
-
如果不使用
std::forward
:std::bind(f, args...)
會導致所有參數都被按值拷貝,或者被錯誤地轉換為左值,可能會產生不必要的性能開銷。
5. 代碼運行流程
auto task = std::make_shared<std::packaged_task<return_type()>>(std::bind(std::forward<F>(f), std::forward<Args>(args)...)
);
執行步驟
-
std::bind(std::forward<F>(f), std::forward<Args>(args)...)
- 綁定
f
和args...
,返回一個可調用對象
- 綁定
-
std::make_shared<std::packaged_task<return_type()>>(...)
- 創建一個
std::packaged_task<return_type()>
,用綁定的函數進行初始化 std::make_shared
進行 一次性分配內存(包括控制塊+對象),提高效率
- 創建一個
-
返回一個
std::shared_ptr<std::packaged_task<return_type()>>
- 用
task->operator()()
執行任務 - 用
task->get_future()
獲取任務結果
- 用
問題4:如何理解tasks.emplace(task { (*task)(); })?
在這段代碼中,tasks.emplace([task]() { (*task)(); })
是一個關鍵操作,它將一個可調用對象(lambda 表達式)加入到 tasks
隊列中。我們可以拆解它的邏輯來理解它的作用。
1. 理解 tasks
tasks
是一個任務隊列,通常是 std::queue<std::function<void()>>
類型的變量,用于存儲需要執行的任務。
std::queue<std::function<void()>> tasks;
因為 std::function<void()>
能夠存儲任意的可調用對象(如函數、lambda、函數對象等),所以我們可以把需要執行的任務封裝進 std::function<void()>
,然后存入隊列。
2. 理解 task
auto task = std::make_shared<std::packaged_task<return_type()>>(std::bind(std::forward<F>(f), std::forward<Args>(args)...)
);
共享智能指針shared_ptr 是一個模板類
// shared_ptr<T> 類模板中,提供了多種實用的構造函數, 語法格式如下:
std::shared_ptr<T> 智能指針名字(創建堆內存);
std::make_shared 是 C++11 標準引入的一個函數模板,用于創建 std::shared_ptr 對象。
這里的 task
是一個 std::shared_ptr<std::packaged_task<return_type()>>
類型的對象,其中 std::packaged_task<return_type()>
封裝了一個可調用對象(比如函數、lambda、成員函數等),當 (*task)()
被調用時,它會執行 f(args...)
,并將結果存入一個 std::future
供外部使用。
3. 為什么要使用智能指針?
在這段代碼中,使用 智能指針 (shared_ptr
) 主要是為了 管理任務的生命周期,具體來說,有以下幾個關鍵原因:
1. 確保任務 (packaged_task
) 的生命周期
在 addTask
函數中,任務 (packaged_task<returntype()>
) 被封裝到一個 shared_ptr
里:
auto task = make_shared<packaged_task<returntype()>> (bind(forward<F>(f), forward<Args>(args)...));
然后,它被存入任務隊列:
queue_Tasksqueue.emplace([task]() {(*task)(); });
這里之所以使用 shared_ptr
,主要是為了 確保 task
在隊列中仍然有效,即:
task
可能會在queue_Tasksqueue
中等待執行,而addTask
可能已經執行完畢并返回。- 如果
task
是 局部變量,那么在addTask
結束時,它會被銷毀,導致 懸空指針或未定義行為。 shared_ptr
允許task
在多個地方安全共享,即使addTask
結束,task
仍然存在,直到任務真正執行完畢。
2. 避免 packaged_task
的拷貝
packaged_task
不允許拷貝,因為它內部維護了一個 future
,拷貝可能導致 future
的所有權問題。
因此,shared_ptr
允許 在多個地方(任務隊列和執行線程)安全傳遞 task
,而無需拷貝。
所以,如果直接使用packaged_task<returntype()> task(bind(forward<F>(f), forward<Args>(args)...));
是不行的,packaged_task類型的task不能拷貝,所以只能用智能指針來管理task。
3. 線程安全,避免懸空任務
任務隊列中的任務可能會在不同線程中執行:
queue_Tasksqueue.emplace([task]() {(*task)(); });
- 任務
task
可能會在 多個線程之間傳遞,如果沒有shared_ptr
,就需要手動管理其生命周期,容易導致 內存泄漏或懸空指針。 shared_ptr
讓任務對象在最后一個線程執行完畢后才會被銷毀,確保 線程安全。
4. 簡化資源管理,避免 new/delete
如果不使用 shared_ptr
,可能需要手動 new
一個 packaged_task
,然后在任務執行完后手動 delete
,容易出錯:
packaged_task<returntype()>* task = new packaged_task<returntype()>(bind(...));
queue_Tasksqueue.emplace([task]() {(*task)(); delete task; });
這樣管理資源容易導致:
- 內存泄漏(忘記
delete
)。 - 懸空指針(
delete
過早執行)。 - 代碼可讀性降低。
使用 shared_ptr
讓 C++ 自動管理 packaged_task
,避免手動 new/delete
的復雜性。
總結
使用 shared_ptr
管理 packaged_task
的生命周期,帶來的好處有:
- 保證任務的生命周期:即使
addTask
結束,任務仍然有效,直到被執行完畢。 - 避免
packaged_task
的拷貝問題:確保future
正確管理。 - 線程安全:任務隊列中的任務可能在多個線程中共享,
shared_ptr
確保不會提前銷毀。 - 避免手動
new/delete
,減少資源管理的復雜性,提高代碼健壯性。
4. 為什么要用Lambda包裝std::shared_ptr<std::packaged_task<returntype()>>類型的task?
1. 直接傳入 task
會導致類型不匹配
線程池的任務隊列 queue_Tasksqueue
是:
queue<function<void(void)>> queue_Tasksqueue;
即,任務隊列存儲的是 std::function<void()>
類型的可調用對象。
而 task
的類型是:
std::shared_ptr<std::packaged_task<returntype()>>
問題:
shared_ptr<packaged_task<returntype()>>
不能隱式轉換為std::function<void()>
。std::function<void()>
需要一個 可調用對象(比如函數、Lambda),但shared_ptr
本身不是可調用對象。
如果你嘗試這樣做:
queue_Tasksqueue.emplace(task); // ? 編譯錯誤
編譯器會報錯,因為 queue_Tasksqueue.emplace()
需要一個 std::function<void()>
,但 shared_ptr<packaged_task<returntype()>>
不是一個可調用對象。
2. 為什么用 lambda
可以解決問題?
我們可以用 Lambda 將 task
變成一個可調用對象:
queue_Tasksqueue.emplace([task]() { (*task)(); });
Lambda 的作用:
- 捕獲
task
(shared_ptr
),保證task
在任務隊列中仍然有效,不會被提前銷毀。 - 使
task
變成一個可調用對象,因為(*task)();
相當于task->operator()()
,執行packaged_task
任務。
這樣,Lambda 的類型就變成了 std::function<void()>
,可以安全地存入 queue_Tasksqueue
。
3. 用 std::bind
也可以
除了 Lambda,你也可以用 std::bind
:
queue_Tasksqueue.emplace(std::bind(&std::packaged_task<returntype()>::operator(), task));
但 Lambda 更直觀,而且 std::bind
可能在一些情況下會導致額外的拷貝,因此 Lambda 是最佳選擇。
4. 總結
寫法 | 是否可行 | 原因 |
---|---|---|
queue_Tasksqueue.emplace(task); | ? 錯誤 | shared_ptr<packaged_task> 不是可調用對象,不能隱式轉換為 std::function<void()> |
queue_Tasksqueue.emplace([task]() { (*task)(); }); | ? 正確 | Lambda 讓 task 變成可調用對象,并確保生命周期管理 |
queue_Tasksqueue.emplace(std::bind(&std::packaged_task<returntype()>::operator(), task)); | ? 正確 | std::bind 也可以包裝 task ,但不如 Lambda 直觀 |
核心結論
shared_ptr<packaged_task>
不是可調用對象,不能直接存入std::function<void()>
。- Lambda 讓
task
變成可調用對象,并確保其生命周期正確管理。 std::bind
也能解決問題,但 Lambda 更直觀。
所以,使用:
queue_Tasksqueue.emplace([task]() { (*task)(); });
是最安全、最直觀的解決方案。🚀
在 ThreadPool::addTask(function<void()>)
這個函數中,不能使用 引用 (const function<void()>& task
) 主要是因為 任務需要被存入隊列并在稍后執行,而 std::function<void()>
可能會封裝臨時對象(如 Lambda)。然而,對于 packaged_task
,情況有所不同。
5. 既然packaged_task 不能被拷貝,那可以用引用傳遞嗎?
1. packaged_task
不能直接用引用
在 ThreadPool::addTask(F&& f, Args&&... args)
這個 模板方法 中:
template<typename F, typename... Args>
future<typename result_of<F(Args...)>::type> addTask(F&& f, Args&&... args)
{using returntype = typename result_of<F(Args...)>::type;// `packaged_task` 綁定函數和參數auto task = make_shared<packaged_task<returntype()>> (bind(forward<F>(f), forward<Args>(args)...));future<returntype> res = task->get_future();mutex_queuemutex.lock();queue_Tasksqueue.emplace([task]() { (*task)(); });mutex_queuemutex.unlock();cv_ConditionVariable.notify_one();return res;
}
在這里,我們使用了 shared_ptr<packaged_task<returntype()>>
,為什么 不能用引用(packaged_task<returntype()>&
)呢?
(1) packaged_task
不能被拷貝
std::packaged_task
不能被拷貝,因為它內部包含 std::future
,拷貝會導致 future
結果管理混亂。所以,如果嘗試這樣做:
queue_Tasksqueue.emplace(*task); // 直接存入隊列
會編譯失敗,因為 packaged_task
沒有拷貝構造函數。
(2) 不能直接使用引用
如果 task
以 引用 (packaged_task<returntype()>&
) 傳遞,而不是 shared_ptr
:
packaged_task<returntype()> task(bind(forward<F>(f), forward<Args>(args)...));
queue_Tasksqueue.emplace([&task]() { task(); });
那么:
task
是局部變量,在addTask
結束時會被銷毀。- 但任務隊列
queue_Tasksqueue
里存儲的 Lambda 可能會在task
被銷毀后才執行,導致 懸空引用,程序崩潰。
(3) 為什么 shared_ptr
可以解決問題
auto task = make_shared<packaged_task<returntype()>> (bind(forward<F>(f), forward<Args>(args)...));
queue_Tasksqueue.emplace([task]() { (*task)(); });
這里使用 shared_ptr
解決了問題:
shared_ptr
允許task
在隊列和執行線程之間共享生命周期。- 只有當任務被執行完畢且
shared_ptr
計數歸零時,packaged_task
才會被銷毀,避免了懸空指針問題。
2. function<void()>
和 packaged_task
的區別
類型 | 可拷貝 | 適合用 shared_ptr 嗎? | 適合用引用嗎? |
---|---|---|---|
std::function<void()> | ? 可以拷貝 | ? 不需要,它本身就是拷貝管理 | ? 不能用引用,可能封裝臨時對象 |
std::packaged_task<R()> | ? 不能拷貝 | ? 需要 shared_ptr 管理生命周期 | ? 不能用引用,可能被提前銷毀 |
3. 總結
-
為什么
std::function<void()>
不能用引用 (&task
)?- 任務可能是 臨時對象(如 Lambda)。
- 任務需要 拷貝存入隊列,引用會導致懸空引用。
-
為什么
std::packaged_task<R()>
需要shared_ptr
?packaged_task
不能拷貝,但需要跨線程傳遞。shared_ptr
確保task
在隊列里存活到執行,防止懸空指針。
因此,在 ThreadPool
里:
std::function<void()>
使用值傳遞,因為它可以安全拷貝。std::packaged_task<R()>
使用shared_ptr
,避免生命周期管理問題。
直接使用引用 (&
) 會導致線程安全問題或懸空指針,因此不適合在這里使用。