線程池類
線程池最主要的職責在於維護任務隊列,調度線程執行任務。
爲此封裝類ThreadPool,提供線程調度方案,定義於頭文件ThreadPool.h,實現於源文件ThreadPool.cpp。
ThreadPool.h
gitee: https://gitee.com/eterfree/ThreadPool/blob/master/src/ThreadPool.h
github: https://github.com/freedomandjustice/ThreadPool/blob/master/src/ThreadPool.h
ThreadPool.cpp
gitee: https://gitee.com/eterfree/ThreadPool/blob/master/src/ThreadPool.cpp
github: https://github.com/freedomandjustice/ThreadPool/blob/master/src/ThreadPool.cpp
ThreadPool.h
類ThreadPool的定義代碼如下所示:
#pragma once
#include <cstddef>
#include <functional>
#include <memory>
#include <list>
class ThreadPool
{
struct Structure;
using DataType = std::shared_ptr<Structure>;
DataType data;
public:
using SizeType = std::size_t;
using Functor = std::function<void()>;
ThreadPool(SizeType threads = getConcurrency(), \
SizeType maxThreads = getConcurrency() << 1U);
ThreadPool(const ThreadPool&) = delete;
ThreadPool(ThreadPool&&) = default;
~ThreadPool();
ThreadPool& operator=(const ThreadPool&) = delete;
ThreadPool& operator=(ThreadPool&&) = default;
static SizeType getConcurrency();
void setMaxThreads(SizeType maxThreads);
SizeType getMaxThreads() const;
SizeType getThreads() const;
SizeType getFreeThreads() const;
SizeType getTasks() const;
void pushTask(Functor&& task);
void pushTask(std::list<Functor>& tasks);
private:
static void setClosed(DataType& data, bool closed);
static bool getClosed(const DataType& data);
static void execute(DataType data);
void destroy();
};
調度設計
任務隊列爲空時,守護線程和工作線程都進入阻塞狀態,一旦新任務到來,立即喚醒守護線程,爲工作線程分配任務。
無空閒線程時,阻塞守護線程,一旦存在空閒線程,喚醒守護線程,爲空閒線程分配任務。
實現調度的關鍵在於運用條件變量,當不滿足執行條件時,通過阻塞和喚醒,及時讓出處理器,在滿足條件之前幾乎不佔用處理器,以提高資源利用率。
成員變量
ThreadPool類採用實現細節隱藏技巧,擁有唯一私有成員變量,只有指針或者引用兩種形式。成員變量名稱爲data,乃智能指針std::shared_ptr對象,指向Structure結構體變量。
實現細節隱藏技巧見章節——程序設計技巧之編譯依存性
智能指針std::shared_ptr自動管理內存,支持ThreadPool與std::thread共享數據,當析構ThreadPool對象或者std::thread的執行函數結束,實現二者任一時候釋放內存。
成員函數
爲保證封裝性,根據訪問情景,ThreadPool類成員函數分爲公有函數和私有函數。
公有函數
函數原型 | 訪問屬性 | 說明 |
---|---|---|
ThreadPool(SizeType threads = getConcurrency(), SizeType maxThreads = getConcurrency() << 1U) | public | 默認構造函數 |
ThreadPool(const ThreadPool&) = delete | public | 刪除默認複製構造函數 |
ThreadPool(ThreadPool&&) = default | public | 應用默認移動構造函數 |
~ThreadPool() | public | 默認析構函數 |
ThreadPool& operator=(const ThreadPool&) = delete | public | 刪除默認複製賦值運算符函數 |
ThreadPool& operator=(ThreadPool&&) = default | public | 應用默認移動賦值運算符函數 |
static SizeType getConcurrency() | public | 獲取支持的併發線程數量 |
void setMaxThreads(SizeType maxThreads) | public | 設置最大線程數量 |
SizeType getMaxThreads() const | public | 獲取最大線程數量 |
SizeType getThreads() const | public | 獲取線程數量 |
SizeType getFreeThreads() const | public | 獲取空閒線程數量 |
SizeType getTasks() const | public | 獲取任務數量 |
void pushTask(Functor&& task) | public | 添加單任務 |
void pushTask(std::list<Functor>& tasks) | public | 批量添加任務 |
私有函數
函數原型 | 訪問屬性 | 說明 |
---|---|---|
static void setClosed(DataType& data, bool closed) | private | 設置關閉狀態 |
static bool getClosed(const DataType& data) | private | 獲取關閉狀態 |
static void execute(DataType data) | private | 守護線程主函數 |
void destroy() | private | 銷燬線程池 |
私有內聯函數作用
ThreadPool類具有關閉狀態,但不直接訪問狀態變量,而提供訪問狀態方法,限定爲私有內聯函數,僅供此類成員函數調用。如此在改變狀態數據結構時,只需修改函數實現,不必更改調用代碼。
靜態成員函數作用
守護線程主函數與訪問狀態方法相似,聲明爲靜態成員函數,除去與類成員指針this的關聯性,以支持類對象的移動語義構造和賦值。
ThreadPool.cpp
引用涉及模塊的頭文件,包括ThreadPool.h、Thread.h和Queue.h三個自定義文件。
其中,Thread.h定義Thread類,Queue.h定義Queue模板。
Thread類的設計詳情見文章——線程類
Queue模板的設計詳情見文章——雙緩衝隊列模板
Thread.h
gitee: https://gitee.com/eterfree/ThreadPool/blob/master/src/Thread.h
github: https://github.com/freedomandjustice/ThreadPool/blob/master/src/Thread.h
Queue.h
gitee: https://gitee.com/eterfree/ThreadPool/blob/master/src/Queue.h
github: https://github.com/freedomandjustice/ThreadPool/blob/master/src/Queue.h
線程池數據結構體的定義代碼如下所示:
#include "ThreadPool.h"
#include "Thread.h"
#include "Queue.h"
#include <utility>
#include <vector>
#include <algorithm>
#include <condition_variable>
struct ThreadPool::Structure
{
using QueueType = Queue<ThreadPool::Functor>;
std::vector<std::unique_ptr<Thread>> threadTable;
std::shared_ptr<QueueType> taskQueue;
std::function<void(bool, Thread::ThreadID)> callback;
std::thread thread;
std::mutex mutex;
std::condition_variable condition;
std::atomic_bool closed;
std::atomic<SizeType> maxThreads;
std::atomic<SizeType> freeThreads;
Structure()
: taskQueue(std::make_shared<QueueType>()) {}
};
代碼解析
- 第23-24行:在構造函數體之外,運用初始化表,構造任務隊列。由於任務隊列乃智能指針std::shared_ptr對象,需要維護引用計數,若調用構造函數(即先以new運算符創建對象,再傳遞給std::shared_ptr),一共申請兩次內存,先申請對象內存,再申請控制塊內存,對象內存和控制塊內存不連續。而使用std::make_shared方法只申請一次內存,對象內存和控制塊內存連續。
數據結構——Structure
結構體數據如下表所示:
變量名 | 類型 | 說明 |
---|---|---|
threadTable | std::vector<std::unique_ptr<Thread>> | 線程表 |
taskQueue | std::shared_ptr<QueueType> | 任務隊列 |
callback | std::function<void(bool, Thread::ThreadID)> | 回調函數子 |
thread | std::thread | 守護線程 |
mutex | std::mutex | 互斥元 |
condition | std::condition_variable | 條件變量 |
closed | std::atomic_bool | 關閉標記 |
maxThreads | std::atomic<SizeType> | 最大線程數量 |
freeThreads | std::atomic<SizeType> | 空閒線程數量 |
成員函數
默認構造函數
ThreadPool::ThreadPool(SizeType threads, SizeType maxThreads)
: data(std::make_shared<Structure>())
{
setClosed(data, false);
setMaxThreads(maxThreads);
threads = std::min(threads, maxThreads);
data->callback = [data = std::weak_ptr(data)](bool free, Thread::ThreadID)
{
if (free)
{
auto shared_data = data.lock();
if (shared_data && ++shared_data->freeThreads == 1U)
shared_data->condition.notify_one();
}
};
data->threadTable.reserve(threads);
for (decltype(threads) counter = 0; counter < threads; ++counter)
{
auto thread = std::make_unique<Thread>();
thread->configure(data->taskQueue, data->callback);
data->threadTable.push_back(std::move(thread));
}
data->freeThreads = data->threadTable.size();
data->thread = std::thread(ThreadPool::execute, data);
}
代碼解析
- 第2行:在構造函數體之外,運用初始化表,構造成員變量,即初始化data只一步。而在函數體之內,初始化分爲兩個步驟,先在函數體外構造data,再於函數體內創建臨時對象並給data賦值。
- 第4行:線程池設爲未關閉狀態。
- 第5-6行:設置最大線程數量和線程數量,保證線程數量不超過最大線程數量。
- 第8-16行:定義Lambda表達式,作爲回調函數子,當線程主動從任務隊列獲取任務失敗時,回調通知線程池,增加空閒線程數量,若增加之前空閒線程數量爲零,守護線程或許處於阻塞狀態,通過條件變量喚醒之。在Lambda表達式之中,以智能指針std::weak_ptr解決std::shared_ptr的循環引用問題,防止銷燬線程池之時資源泄漏,詳情見文章——智能指針std::shared_ptr的循環引用問題。
- 第18行:預分配內存空間,但不初始化內存,即未調用構造函數。
- 第19-24行:創建一定線程,分別配置任務隊列和回調函數子,並且放入線程表。
- 第25行:設置空閒線程數量爲新建線程數量。
- 第26行:創建std::thread對象,即守護線程,以data爲參數,執行execute函數。
默認析構函數
調用銷燬方法,自動關閉守護線程,銷燬線程池。
ThreadPool::~ThreadPool()
{
destroy();
}
獲取支持的併發線程數量
獲取硬件設備與操作系統支持的併發線程數量。
ThreadPool::SizeType ThreadPool::getConcurrency()
{
return std::thread::hardware_concurrency();
}
訪問線程數量方法
訪問線程數量方法用以支持拓展功能,包括設置最大線程數量、獲取最大線程數量、獲取線程數量、獲取空閒線程數量。
void ThreadPool::setMaxThreads(SizeType maxThreads)
{
data->maxThreads = maxThreads > 0U ? maxThreads : 1U;
}
ThreadPool::SizeType ThreadPool::getMaxThreads() const
{
return data->maxThreads;
}
ThreadPool::SizeType ThreadPool::getThreads() const
{
return data->threadTable.size();
}
ThreadPool::SizeType ThreadPool::getFreeThreads() const
{
return data->freeThreads;
}
獲取任務數量方法
任務隊列的任務數量在變化,獲取的數量只是一時刻的參考數據。
ThreadPool::SizeType ThreadPool::getTasks() const
{
return data->taskQueue->size();
}
添加單任務方法
被添加的任務或許是空任務, 給線程配置空任務會成功,而啓動線程會失敗,導致守護線程失去啓動線程的能力。在最壞的情況,所有線程處於阻塞狀態,而線程池無法處理任務,當任務堆積過多,內存耗盡,最終程序崩潰。
void ThreadPool::pushTask(Functor&& task)
{
if (task == nullptr) return;
data->taskQueue->push(std::move(task));
if (data->taskQueue->size() == 1U)
data->condition.notify_one();
}
代碼解析
- 第3行:過濾空任務,防止守護線程配置任務時無法啓動線程,以增強線程池的健壯性。
- 第4行:移動任務至任務隊列。
- 第5-6行:如果添加任務之前,任務隊列爲空,守護線程或許處於阻塞狀態,通過條件變量喚醒之。
批量添加任務方法
放入任務至任務隊列,涉及鎖定和釋放互斥元,若遇到突發性的大量任務,調用添加單任務方法,會頻繁操作互斥元,對效率影響大。
於是,定義批量添加任務方法,形成任務隊列之外的任務緩衝區,避免頻繁操作互斥元。
不過,批量添加任務存在誤區,當任務隊列爲空時,如果仍在緩存任務,而未放入任務隊列,線程池被閒置,資源未得到利用,任務未被及時執行。
最好的方案是調用獲取任務數量方法,根據任務隊列剩餘任務數量,選擇添加任務方式。若剩餘任務偏少,調用添加單任務方法,否則先緩存任務,當剩餘任務偏少時,再調用批量添加任務方法。
void ThreadPool::pushTask(std::list<Functor>& tasks)
{
for (auto it = tasks.cbegin(); it != tasks.cend();)
if (*it) ++it;
else it = tasks.erase(it);
if (auto size = tasks.size(); size > 0U)
{
data->taskQueue->push(tasks);
if (data->taskQueue->size() == size)
data->condition.notify_one();
}
}
代碼解析
- 第3-5行:過濾空任務,防止守護線程配置任務時無法啓動線程,以增強線程池的健壯性。
- 第6-8行:若存在有效任務,將其放入任務隊列。
- 第9-10行:如果添加任務之前,任務隊列爲空,守護線程或許處於阻塞狀態,通過條件變量喚醒之。
訪問狀態方法
訪問狀態方法顯喻爲內聯函數,包括設置關閉狀態、獲取關閉狀態,僅支持內聯於同一文件同類成員函數。
inline void ThreadPool::setClosed(DataType& data, bool closed)
{
data->closed = closed;
}
inline bool ThreadPool::getClosed(const DataType& data)
{
return data->closed;
}
守護線程主函數
void ThreadPool::execute(DataType data)
{
using std::defer_lock;
std::unique_lock threadLocker(data->mutex, defer_lock);
std::unique_lock taskLocker(data->taskQueue->mutex(), defer_lock);
while (!getClosed(data))
{
threadLocker.lock();
data->condition.wait(threadLocker, \
[&data] { return data->freeThreads || getClosed(data); });
if (getClosed(data)) break;
for (auto it = data->threadTable.begin(); \
it != data->threadTable.end() \
&& data->freeThreads \
&& !getClosed(data); ++it)
{
if (auto& thread = *it; thread->free())
{
taskLocker.lock();
data->condition.wait(taskLocker, \
[&data] { return !data->taskQueue->empty() || getClosed(data); });
if (getClosed(data)) return;
if (thread->configure(data->taskQueue->front()) \
&& thread->start())
{
data->taskQueue->pop();
--data->freeThreads;
}
taskLocker.unlock();
}
}
threadLocker.unlock();
}
}
代碼解析
- 第3-4行:創建std::unique_lock對象,作爲線程互斥鎖,指定延遲鎖定策略,用於互斥訪問線程表。由於析構互斥鎖之時,自動釋放互斥元,因此不必手動釋放。
- 第5行:創建任務互斥鎖,延遲鎖定任務隊列互斥元,用於從任務隊列互斥讀取任務。
- 第7、12、24行:若線程池設爲關閉狀態,退出循環,結束守護線程。設立三個退出通道,保證守護線程在執行時、因無空閒線程或者任務隊列爲空而阻塞都能夠及時結束,以提高響應敏捷度。
- 第9、35行:訪問線程表之前,先鎖定線程互斥元;在訪問之後,釋放互斥元。
- 第10-11行:檢查空閒線程數量和線程池關閉狀態。如果無空閒線程並且線程池未關閉,守護進程進入阻塞狀態,等待條件變量的喚醒信號;否則守護線程繼續順序執行指令。當條件變量喚醒守護進程時,再次檢查空閒線程數量和線程池關閉狀態。
- 第14-17行:當線程池未關閉,並且空閒線程數量非零,遍歷線程表尋找空閒線程,爲其分配任務。
- 第19行:若線程處於空閒狀態,則進行任務配置步驟。
- 第21、32行:訪問任務隊列之前,鎖定任務隊列互斥元;訪問之後,釋放互斥元。
- 第26-27行:爲線程分配新任務,並且啓動線程執行任務。
- 第29-30行:從任務隊列彈出已配置的任務,並且減少空閒線程數量。
銷燬方法
線程池設爲關閉狀態,並喚醒守護線程,由守護線程銷燬線程,以支持非阻塞式銷燬線程池。
void ThreadPool::destroy()
{
if (data == nullptr || getClosed(data))
return;
setClosed(data, true);
data->thread.detach();
data->condition.notify_one();
}
代碼解析
- 第3-4行:若數據爲空或者線程池已關閉,無需銷燬線程池,直接退出函數。判斷數據爲空乃關鍵步驟,用以支持移動語義。
- 第5行:線程池設爲關閉狀態,即銷燬狀態。
- 第7行:分離線程池的守護線程,非阻塞式銷燬線程池。
- 第8行:守護線程或許處於阻塞狀態,通過條件變量喚醒之。