C++17 線程池類


線程池最主要的職責在於維護任務隊列,調度線程執行任務。
爲此封裝類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行:守護線程或許處於阻塞狀態,通過條件變量喚醒之。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章