线程类
线程类提供线程重用方案,支持销毁再创建,一次创建反复使用。
设计原理
任务配置设计
线程类灵活性高,既作为独立模块,也作为线程池的线程,既可以配置单任务,也可以配置任务队列。
线程类支持配置任务队列,可以实现单线程独占任务队列,按照出队列之顺序执行任务;也可以实现多线程共享任务队列,不过需要确保队列接口线程安全。
文件依赖性
线程类Thread定义于头文件Thread.h,实现于源文件Thread.cpp。
Thread.h
Thread.cpp
- GitCode: Thread.cpp
- Gitee: Thread.cpp
- GitHub: Thread.cpp
线程的阻塞与激活依赖于强化条件类模板Condition。强化条件类模板Condition用于精准控制阻塞与激活,当激活先于阻塞时,确保线程正常退出,其定义于头文件Condition.hpp。
Condition.hpp
- GitCode: Condition.hpp
- Gitee: Condition.hpp
- GitHub: Condition.hpp
强化条件类模板Condition的设计与实现见文章——强化条件变量。
关于激活先于阻塞,更多介绍见章节——程序设计技巧之激活先于阻塞。
线程类Thread聚合双缓冲队列类模板DoubleQueue,定义于头文件DoubleQueue.hpp。
DoubleQueue.hpp
- GitCode: DoubleQueue.hpp
- Gitee: DoubleQueue.hpp
- GitHub: DoubleQueue.hpp
双缓冲队列类模板DoubleQueue的设计与实现见文章——双缓冲队列类模板。
编译依存性降低策略
在Thread.h声明类模板DoubleQueue,在Thread.cpp引用DoubleQueue.hpp,定义类模板DoubleQueue,从而降低文件间的编译依存性。
文件间的编译依存性见文章——编译依存性。
类定义
类Thread定义于头文件Thread.h,代码如下所示:
#pragma once
#include <functional>
#include <memory>
#include <mutex>
#include <thread>
template <typename _Element>
class DoubleQueue;
class Thread final
{
struct Structure;
private:
using DataType = std::shared_ptr<Structure>;
public:
using TaskType = std::function<void()>;
using QueueType = std::shared_ptr<DoubleQueue<TaskType>>;
using ThreadID = std::thread::id;
using Callback = std::function<void(ThreadID, bool)>;
private:
mutable std::mutex _mutex;
DataType _data;
private:
static DataType move(Thread& _left, \
Thread&& _right);
static void destroy(DataType&& _data);
static bool getTask(DataType& _data);
static void execute(DataType _data);
private:
auto load() const
{
std::lock_guard lock(_mutex);
return _data;
}
public:
Thread();
Thread(const Thread&) = delete;
Thread(Thread&& _another) noexcept;
~Thread() noexcept;
Thread& operator=(const Thread&) = delete;
Thread& operator=(Thread&& _thread) noexcept;
ThreadID getID() const;
bool idle() const;
bool create();
void destroy()
{
destroy(load());
}
bool configure(const QueueType& _taskQueue, \
const Callback& _callback);
bool configure(const TaskType& _task, \
const Callback& _callback);
bool configure(TaskType&& _task, \
const Callback& _callback);
bool notify();
};
成员变量
线程类采用实现细节隐藏技巧,仅有互斥元_mutex和共享指针_data两个私有成员变量。
以共享指针std::shared_ptr变量,指向结构体Structure实例,实现自动管理内存,支持Thread与std::thread共享数据,在Thread实例析构之时,或者当std::thread执行结束时,二者任一时机释放内存。
为了隐藏类的属性与行为,成员变量可选指针或者引用。
关于实现细节隐藏技巧,更多介绍见章节——编译依存性之实现细节隐藏技巧。
成员函数
数据共享
线程主函数与获取任务函数声明为静态成员,除去与类成员指针this的关联性,以支持移动语义构造和赋值类实例。同时降低Thread与std::thread的耦合性,并且二者共享成员结构体数据。
封装性
根据访问情景,限定仅由类成员函数调用的成员函数为私有成员。删除默认复制构造函数和默认复制赋值运算符函数,实现默认移动构造函数和默认移动赋值运算符函数。
线程安全性
线程类接口通过互斥元构建临界区,以原子操作获取共享指针,而线程主函数亦持有共享指针,既确保接口的线程安全性,又不影响线程性能。
类实现
数据结构
结构体Structure定义于源文件Thread.cpp,代码如下所示:
#include "Thread.h"
#include "Condition.hpp"
#include "DoubleQueue.hpp"
#include <utility>
#include <cstdint>
#include <exception>
#include <iostream>
#include <sstream>
#include <atomic>
struct Thread::Structure
{
enum class State : std::uint8_t
{
EMPTY,
INITIAL,
RUNNABLE,
RUNNING,
BLOCKED,
};
std::mutex _threadMutex;
std::thread _thread;
Condition<> _condition;
std::atomic<State> _state;
mutable std::mutex _taskMutex;
TaskType _task;
QueueType _taskQueue;
Callback _callback;
Structure() : _state(State::EMPTY) {}
auto getID() const noexcept
{
return _thread.get_id();
}
auto getState() const noexcept
{
return _state.load(std::memory_order_relaxed);
}
void setState(State _state) noexcept
{
this->_state.store(_state, \
std::memory_order_relaxed);
}
bool getValidity() const
{
std::lock_guard lock(_taskMutex);
return static_cast<bool>(_task);
}
bool getTask(TaskType& _task);
void setTask(const TaskType& _task)
{
std::lock_guard lock(_taskMutex);
this->_task = _task;
}
void setTask(TaskType&& _task)
{
std::lock_guard lock(_taskMutex);
this->_task = std::forward<TaskType>(_task);
}
};
代码解析
- 第14-21行:定义强枚举State,列举线程状态,包括空状态、初始状态、就绪状态、运行状态、阻塞状态。
结构体的成员变量如表所示:
变量名 | 类型 | 说明 |
---|---|---|
_threadMutex | std::mutex | 线程互斥元 |
_thread | std::thread | 线程实体 |
_condition | Condition<> | 强化条件变量 |
_state | std::atomic<State> | 原子状态 |
_taskMutex | std::mutex | 任务互斥元 |
_task | TaskType | 任务函数子 |
_taskQueue | QueueType | 任务队列 |
_callback | Callback | 回调函数子 |
成员函数
数据结构体
获取任务
以移动语义获取任务,并确保操作原子性。
bool Thread::Structure::getTask(TaskType& _task)
{
std::lock_guard lock(_taskMutex);
_task = std::move(this->_task);
return static_cast<bool>(_task);
}
代码解析
- 第4行:以移动语义赋值,支持移动任务,避免复制数据。
- 第5行:返回任务有效性。
线程类
移动数据
实现移动语义赋值,并确保操作原子性。
auto Thread::move(Thread& _left, Thread&& _right) \
-> DataType
{
std::lock_guard leftLock(_left._mutex);
auto data = std::move(_left._data);
std::lock_guard rightLock(_right._mutex);
_left._data = std::move(_right._data);
return data;
}
代码解析
- 第4,7行:分别锁定两个互斥元,以减小互斥粒度。
- 第5,9行:获取并返回原始数据。
销毁线程
通知并等待线程退出,而后重置共享数据。
void Thread::destroy(DataType&& _data)
{
using State = Structure::State;
if (!_data) return;
std::lock_guard lock(_data->_threadMutex);
if (_data->getState() == State::EMPTY)
return;
_data->_condition.exit();
if (_data->_thread.joinable())
_data->_thread.join();
_data->_taskQueue.reset();
_data->_callback = nullptr;
_data->setState(State::EMPTY);
}
代码解析
- 第8,9行:倘若线程处于空状态,不必销毁线程而直接退出函数。
- 第11行:条件无效化,并且通知线程退出。
- 第13,14行:挂起直到线程退出为止。
- 第16-18行:重置任务队列与回调函数子,并且线程转为空状态。
获取任务
从任务队列取出任务,之后线程转为就绪状态。
bool Thread::getTask(DataType& _data)
{
using State = Structure::State;
if (!_data->_taskQueue)
return false;
decltype(_data->_task) task;
if (!_data->_taskQueue->pop(task))
return false;
if (!task) task = [] {};
_data->setState(State::RUNNABLE);
_data->setTask(std::move(task));
return true;
}
代码解析
- 第5,6行:若未配置任务队列,则获取任务失败。
- 第8-10行:若从任务队列取出任务失败,则获取任务失败。
- 第12行:防止获取到无效任务而非预期阻塞线程。
- 第14,15行:线程转为就绪状态,并为其配置任务。
线程主函数
创建线程并立即阻塞线程,直到配置任务或者任务队列,才可以激活线程。在执行任务之后,若配有任务队列,则主动获取任务。若获取任务失败,则再次阻塞线程。
void Thread::execute(DataType _data)
{
using State = Structure::State;
auto predicate = [&_data]
{ return _data->getValidity(); };
_data->_condition.wait(predicate);
while (_data->_condition \
|| _data->getValidity())
{
_data->setState(State::RUNNING);
try
{
if (decltype(_data->_task) task; \
_data->getTask(task)) task();
}
catch (std::exception& exception)
{
std::ostringstream stream;
stream << exception.what() << std::endl;
std::clog << stream.str();
}
auto callback = _data->_callback;
bool idle = !getTask(_data);
if (idle)
_data->setState(State::BLOCKED);
if (callback)
callback(_data->getID(), idle);
_data->_condition.wait(predicate);
}
}
代码解析
- 第5,6行:定义谓词,若任务有效,则线程不必等待。
- 第8,36行:倘若谓词非真,自动解锁互斥元,并阻塞线程,直至收到通知,激活线程,再次锁定互斥元。
- 第10,11行:若条件或者任务有效,则继续循环,确保线程在退出之前,执行所有任务。
- 第13行:线程设为运行状态。
- 第15-25行:在执行任务之时捕获异常,应对任务函数子抛出异常,防止线程泄漏,甚至程序崩溃。
关于线程泄漏,更多介绍见章节——程序设计技巧之线程泄漏。
- 第17,18行:若任务有效,则执行任务。
- 第27行:缓存回调函数子,避免在线程设为闲置状态之后,配置任务与回调函数子,引发多线程数据竞争问题。
- 第29行:主动获取任务,取反结果,即线程是否闲置。
- 第30,31行:若线程闲置,则设为阻塞状态。
- 第33,34行:若存在回调函数子,则以线程唯一标识与闲置状态为参数,执行回调函数子。
默认构造函数
初始化共享数据,并且创建线程。
Thread::Thread() : \
_data(std::make_shared<Structure>())
{
create();
}
代码解析
- 第2行:在构造函数体之外,采用成员初始化列表,构造成员变量,即一步初始成员变量_data。而在函数体之内,初始化分为两个步骤,先在函数体外构造_data,后在函数体内对_data赋值。
默认移动构造函数
以互斥锁确保线程安全性,并且转移线程所有权。
Thread::Thread(Thread&& _another) noexcept
{
try
{
std::lock_guard lock(_another._mutex);
this->_data = std::move(_another._data);
}
catch (std::exception&) {}
}
代码解析
- 第3,4,7,8行:捕获异常,确保移动构造函数的异常安全性。
默认析构函数
销毁线程,确保析构函数的异常安全性。
Thread::~Thread() noexcept
{
try
{
destroy();
}
catch (std::exception&) {}
}
默认移动赋值运算符函数
转移线程所有权,并销毁原始线程。
auto Thread::operator=(Thread&& _thread) noexcept
-> Thread&
{
if (&_thread != this)
{
try
{
auto data = move(*this, \
std::forward<Thread>(_thread));
destroy(std::move(data));
}
catch (std::exception&) {}
}
return *this;
}
代码解析
- 第4行:确保二者为不同实例,避免二者为相同实例,从而导致异常销毁线程。
- 第8,9行:移动赋值共享数据,获取原始共享数据。
- 第11行:销毁原始线程。
获取线程唯一标识
获取线程唯一标识,用于区分不同线程实例。
auto Thread::getID() const -> ThreadID
{
auto data = load();
if (!data) return ThreadID();
std::lock_guard lock(data->_threadMutex);
return data->getID();
}
代码解析
- 第3行:原子加载共享指针。
- 第4行:若共享指针为空,即无共享数据,则返回无效线程标识。
- 第6行:以互斥元确保接口线程安全。
判断闲置
以线程状态为依据,判断线程是否闲置。
bool Thread::idle() const
{
using State = Structure::State;
auto data = load();
if (!data) return false;
auto state = data->getState();
return state == State::INITIAL \
|| state == State::BLOCKED;
}
代码解析
- 第8-10行:闲置状态包括初始状态和阻塞状态。
创建线程
默认构造函数自动调用创建线程函数。支持销毁再创建,在调用销毁线程函数之后,可以再次调用创建线程函数。
bool Thread::create()
{
using State = Structure::State;
auto data = load();
if (!data) return false;
std::lock_guard lock(data->_threadMutex);
if (data->getState() != State::EMPTY)
return false;
data->setState(State::INITIAL);
data->_condition.enter();
data->_thread = std::thread(execute, data);
return true;
}
代码解析
- 第9,10行:倘若线程并非空状态,返回创建失败。
- 第12行:线程转为初始状态。
- 第14行:强化条件变量有效化以反复使用。
- 第16行:创建std::thread实例,以共享指针data为参数,执行函数execute。
配置任务队列与回调函数子
可选配置任务队列与回调函数子,分别用于自动获取任务,以及在执行任务之后,通知任务所有者,传递线程闲置状态。
bool Thread::configure(const QueueType& _taskQueue, \
const Callback& _callback)
{
if (!_taskQueue) return false;
auto data = load();
if (!data) return false;
std::lock_guard lock(data->_threadMutex);
if (!idle()) return false;
data->_taskQueue = _taskQueue;
data->_callback = _callback;
data->setState(Structure::State::BLOCKED);
return true;
}
代码解析
- 第4行:倘若任务队列无效,返回配置失败。
- 第10行:倘若线程并非闲置状态,禁止配置任务队列和回调函数子,返回配置失败。
- 第12-14行:配置任务队列与回调函数子,线程转为阻塞状态。
配置单任务与回调函数子
支持配置单任务和回调函数子,可选复制语义和移动语义。
bool Thread::configure(const TaskType& _task, \
const Callback& _callback)
{
if (!_task) return false;
auto data = load();
if (!data) return false;
std::lock_guard lock(data->_threadMutex);
if (!idle()) return false;
data->setState(Structure::State::RUNNABLE);
data->_callback = _callback;
data->setTask(_task);
return true;
}
bool Thread::configure(TaskType&& _task, \
const Callback& _callback)
{
if (!_task) return false;
auto data = load();
if (!data) return false;
std::lock_guard lock(data->_threadMutex);
if (!idle()) return false;
data->setState(Structure::State::RUNNABLE);
data->_callback = _callback;
data->setTask(std::forward<TaskType>(_task));
return true;
}
代码解析
- 第4,21行:倘若任务无效,返回配置失败。
- 第10,27行:倘若线程并非闲置状态,禁止配置单任务和回调函数子,返回配置失败。
- 第12-14,29-31行:线程转为就绪状态,配置回调和任务函数子。
激活线程
对于配置任务队列,先获取任务,再激活线程。而对于配置单任务,直接激活线程。
bool Thread::notify()
{
using Policy = Condition<>::Policy;
using State = Structure::State;
auto data = load();
if (!data)
return false;
std::lock_guard lock(data->_threadMutex);
auto state = data->getState();
if (state == State::BLOCKED \
&& getTask(data))
state = State::RUNNABLE;
if (state != State::RUNNABLE)
return false;
data->_condition.notify_one(Policy::RELAXED);
return true;
}
代码解析
- 第13-15行:若线程处于阻塞状态,则获取任务。当获取任务成功时,线程转为就绪状态。
- 第17,18行:若线程并非就绪状态,直接退出函数,而不必激活线程。
- 第20行:通过条件变量激活线程。