C++17 线程类


线程类提供线程重用方案,支持销毁再创建,一次创建反复使用。

设计原理

任务配置设计

线程类灵活性高,既作为独立模块,也作为线程池的线程,既可以配置单任务,也可以配置任务队列。

线程类支持配置任务队列,可以实现单线程独占任务队列,按照出队列之顺序执行任务;也可以实现多线程共享任务队列,不过需要确保队列接口线程安全。

文件依赖性

线程类Thread定义于头文件Thread.h,实现于源文件Thread.cpp。

Thread.h

Thread.cpp

线程的阻塞与激活依赖于强化条件类模板Condition。强化条件类模板Condition用于精准控制阻塞与激活,当激活先于阻塞时,确保线程正常退出,其定义于头文件Condition.hpp。

Condition.hpp

强化条件类模板Condition的设计与实现见文章——强化条件变量
关于激活先于阻塞,更多介绍见章节——程序设计技巧之激活先于阻塞。

线程类Thread聚合双缓冲队列类模板DoubleQueue,定义于头文件DoubleQueue.hpp。

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,列举线程状态,包括空状态、初始状态、就绪状态、运行状态、阻塞状态。

结构体的成员变量如表所示:

变量名类型说明
_threadMutexstd::mutex线程互斥元
_threadstd::thread线程实体
_conditionCondition<>强化条件变量
_statestd::atomic<State>原子状态
_taskMutexstd::mutex任务互斥元
_taskTaskType任务函数子
_taskQueueQueueType任务队列
_callbackCallback回调函数子

成员函数

数据结构体

获取任务

以移动语义获取任务,并确保操作原子性。

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行:通过条件变量激活线程。
C++是一种面向对象的计算机程序设计语言,由美国AT&T贝尔实验室的本贾尼·斯特劳斯特卢普博士在20世纪80年代初期发明并实现(最初这种语言被称作“C with Classes”带的C)。它是一种静态数据型检查的、支持多重编程范式的通用程序设计语言。它支持过程化程序设计、数据抽象、面向对象程序设计、泛型程序设计等多种程序设计风格。C++是C语言的继承,进一步扩充和完善了C语言,成为一种面向对象的程序设计语言。C++这个词在中国大陆的程序员圈子中通常被读做“C加加”,而西方的程序员通常读做“C plus plus”,“CPP”。 在C基础上,一九八三年又由贝尔实验室的Bjarne Strou-strup推出了C++C++进一步扩充和完善了C语言,成为一种面向 对象的程序设计语言。C++目前流行的编译器最新版本是Borland C++ 4.5,Symantec C++ 6.1,和Microsoft Visual C++ 2012。C++提出了一些更为深入的概念,它所支持的这些面向对象的概念容易将问题空间直接地映射到程序空间,为程序员提供了一种与传统结构程序设计不同的思维方式和编程方法。因而也增加了整个语言的复杂性,掌握起来有一定难度。C++由美国AT&T贝尔实验室的本贾尼·斯特劳斯特卢普博士在20世纪80年代初期发明并实现(最初这种语言被称作“C with Classes”带的C)。开始,C++是作为C语言的增强版出现的,从给C语言增加开始,不断的增加新特性。虚函数(virtual function)、运算符重载(Operator Overloading)、多重继承(Multiple Inheritance)、模板(Template)、异常(Exception)、RTTI、命名空间(Name Space)逐渐被加入标准。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值