C++17 双缓冲队列类模板


设计并实现轻量级高并发线程池,命名为eterfree::ThreadPool,简称为Eterfree线程池。

前序

初衷

Eterfree线程池以一个基于Boost程序库的线程池(简称为参照线程池)为设计目标,希望调度线程执行任务的效率超过参照线程池。

困境

梦想总是美好的,而现实总是残酷的。

Eterfree线程池的初始性能并未优于参照线程池。

突破

无论正在经历什么,都不要轻易放弃,因为从没有一种坚持会被辜负。

初步测试,向任务队列放入任务之时,Eterfree线程池更耗时。经过分析,放入和取出任务操作互斥,频繁放入任务会影响线程获取任务,从而降低线程池性能。

因此决定优化任务队列,封装双缓冲队列类模板。仅当取出元素而出口队列为空时,访问两个队列才相互影响,似此出入队列得以提高效率。

结果

然而,战胜他人无用,须得超越自己。

通过引入双缓冲队列,提升线程池调度效率。最终,Eterfree线程池与参照线程池在伯仲之间,或许在性能上略优于参照线程池。

文件依赖性

双缓冲队列类模板不依赖标准库以外的文件,定义于头文件DoubleQueue.hpp。

DoubleQueue.hpp

类定义

双缓冲队列类模板的定义如下所示:

#pragma once

#include <optional>
#include <utility>
#include <list>
#include <atomic>
#include <mutex>

template <typename _Element>
class DoubleQueue final
{
public:
	using Element = _Element;
	using QueueType = std::list<Element>;
	using SizeType = typename QueueType::size_type;

private:
	using Atomic = std::atomic<SizeType>;
	using MutexType = std::mutex;

private:
	Atomic _capacity;
	Atomic _size;

	mutable MutexType _entryMutex;
	QueueType _entryQueue;

	mutable MutexType _exitMutex;
	QueueType _exitQueue;

private:
	static auto get(const Atomic& _atomic) noexcept
	{
		return _atomic.load(std::memory_order_relaxed);
	}

	static void set(Atomic& _atomic, SizeType _size) noexcept
	{
		_atomic.store(_size, std::memory_order_relaxed);
	}

	static auto exchange(Atomic& _atomic, SizeType _size) noexcept
	{
		return _atomic.exchange(_size, std::memory_order_relaxed);
	}

	static void copy(DoubleQueue& _left, const DoubleQueue& _right);

	static void move(DoubleQueue& _left, DoubleQueue&& _right) noexcept;

private:
	auto add(SizeType _size) noexcept
	{
		return this->_size.fetch_add(_size, \
			std::memory_order_relaxed);
	}

	auto subtract(SizeType _size) noexcept
	{
		return this->_size.fetch_sub(_size, \
			std::memory_order_relaxed);
	}

	bool valid(QueueType& _queue) const noexcept;

public:
	DoubleQueue(SizeType _capacity = 0) : \
		_capacity(_capacity), _size(0) {}

	DoubleQueue(const DoubleQueue& _another);

	DoubleQueue(DoubleQueue&& _another);

	DoubleQueue& operator=(const DoubleQueue& _doubleQueue);

	DoubleQueue& operator=(DoubleQueue&& _doubleQueue);

	auto capacity() const noexcept
	{
		return get(_capacity);
	}

	void reserve(SizeType _capacity) noexcept
	{
		set(this->_capacity, _capacity);
	}

	auto size() const noexcept { return get(_size); }
	bool empty() const noexcept { return size() == 0; }

	std::optional<SizeType> push(const Element& _element);
	std::optional<SizeType> push(Element&& _element);

	std::optional<SizeType> push(QueueType& _queue);
	std::optional<SizeType> push(QueueType&& _queue);

	bool pop(Element& _element);
	std::optional<Element> pop();

	bool pop(QueueType& _queue);

	SizeType clear();
};

成员变量

双缓冲队列类模板的成员变量如表所示:

变量名类型说明
_capacityAtomicType队列容量
_sizeAtomicType元素数量
_entryMutexmutable MutexType入口互斥元
_entryQueueQueueType入口队列
_exitMutexmutable MutexType出口互斥元
_exitQueueQueueType出口队列

双缓冲队列分为入口队列与出口队列。在访问两个队列之时,分别采用入口互斥元和出口互斥元,旨在灵活地控制互斥粒度。

元素数量采用原子计数,在多线程环境,以最小互斥粒度确保元素数量准确无误。

成员函数

完全复制

复制双缓冲队列的可复制成员变量,包括出口队列与入口队列,元素数量和队列容量。

template <typename _Element>
void DoubleQueue<_Element>::copy(DoubleQueue& _left, \
	const DoubleQueue& _right)
{
	_left._exitQueue = _right._exitQueue;
	_left._entryQueue = _right._entryQueue;
	set(_left._size, get(_right._size));
	set(_left._capacity, get(_right._capacity));
}

完全移动

移动双缓冲队列的可移动成员变量,包括出口队列与入口队列,元素数量和队列容量。

template <typename _Element>
void DoubleQueue<_Element>::move(DoubleQueue& _left, \
	DoubleQueue&& _right) noexcept
{
	_left._exitQueue = std::move(_right._exitQueue);
	_left._entryQueue = std::move(_right._entryQueue);
	set(_left._size, exchange(_right._size, 0));
	set(_left._capacity, exchange(_right._capacity, 0));
}

代码解析

  • 第7,8行:获取并重置来源双缓冲队列的元素数量和队列容量,以获取结果设置目的双缓冲队列的元素数量和队列容量。

检验容量限制

任务队列用以批量放入任务,检验其是否超出容量限制。若未超出容量限制,则任务队列有效。

template <typename _Element>
bool DoubleQueue<_Element>::valid(QueueType& _queue) const noexcept
{
	auto capacity = this->capacity();
	if (capacity <= 0) return true;

	auto size = this->size();
	return size < capacity and _queue.size() <= capacity - size;
}

代码解析

  • 第4,5行:队列容量小于等于零,表明无容量限制。

复制构造函数

注意互斥元锁定顺序与取出元素函数一致,避免因锁定顺序不同,而彼此等待释放互斥元,以致出现死锁问题。

template <typename _Element>
DoubleQueue<_Element>::DoubleQueue(const DoubleQueue& _another)
{
	std::scoped_lock lock(_another._exitMutex, \
		_another._entryMutex);
	copy(*this, _another);
}

移动构造函数

注意互斥元锁定顺序与取出元素函数一致,避免因锁定顺序不同,而彼此等待释放互斥元,以致出现死锁问题。

template <typename _Element>
DoubleQueue<_Element>::DoubleQueue(DoubleQueue&& _another)
{
	std::scoped_lock lock(_another._exitMutex, \
		_another._entryMutex);
	move(*this, std::forward<DoubleQueue>(_another));
}

复制赋值运算符函数

注意互斥元锁定顺序与取出元素函数一致,避免因锁定顺序不同,而彼此等待释放互斥元,以致出现死锁问题。

template <typename _Element>
auto DoubleQueue<_Element>::operator=(const DoubleQueue& _doubleQueue) \
-> DoubleQueue&
{
	if (&_doubleQueue != this)
	{
		std::scoped_lock lock(this->_exitMutex, this->_entryMutex, \
			_doubleQueue._exitMutex, _doubleQueue._entryMutex);
		copy(*this, _doubleQueue);
	}
	return *this;
}

代码解析

  • 第5行:避免对自己赋值。

移动赋值运算符函数

注意互斥元锁定顺序与取出元素函数一致,避免因锁定顺序不同,而彼此等待释放互斥元,以致出现死锁问题。

template <typename _Element>
auto DoubleQueue<_Element>::operator=(DoubleQueue&& _doubleQueue) \
-> DoubleQueue&
{
	if (&_doubleQueue != this)
	{
		std::scoped_lock lock(this->_exitMutex, this->_entryMutex, \
			_doubleQueue._exitMutex, _doubleQueue._entryMutex);
		move(*this, std::forward<DoubleQueue>(_doubleQueue));
	}
	return *this;
}

放入单个元素

向入口队列放入单个元素,可选复制语义或者移动语义。

template <typename _Element>
auto DoubleQueue<_Element>::push(const Element& _element) \
-> std::optional<SizeType>
{
	std::lock_guard lock(_entryMutex);
	if (auto capacity = this->capacity(); \
		capacity > 0 && size() >= capacity)
		return std::nullopt;

	_entryQueue.push_back(_element);
	return add(1);
}

template <typename _Element>
auto DoubleQueue<_Element>::push(Element&& _element) \
-> std::optional<SizeType>
{
	std::lock_guard lock(_entryMutex);
	if (auto capacity = this->capacity(); \
		capacity > 0 && size() >= capacity)
		return std::nullopt;

	_entryQueue.push_back(std::forward<Element>(_element));
	return add(1);
}

代码解析

  • 第6-8,19-21行:如果队列容量大于零,表明队列容量有限制。若元素数量大于等于容量,则返回空值,即放入元素失败。
  • 第10,11,23,24行:放入元素并返回放入之前的元素数量。

批量放入元素

在双缓冲队列之外,创建入口队列同类型的队列实例,用以缓存多元素,再批量转移元素至入口队列。

template <typename _Element>
auto DoubleQueue<_Element>::push(QueueType& _queue) \
-> std::optional<SizeType>
{
	std::lock_guard lock(_entryMutex);
	if (not valid(_queue)) return std::nullopt;

	auto size = _queue.size();
	_entryQueue.splice(_entryQueue.cend(), _queue);
	return add(size);
}

template <typename _Element>
auto DoubleQueue<_Element>::push(QueueType&& _queue) \
-> std::optional<SizeType>
{
	std::lock_guard lock(_entryMutex);
	if (not valid(_queue)) return std::nullopt;

	auto size = _queue.size();
	_entryQueue.splice(_entryQueue.cend(), \
		std::forward<QueueType>(_queue));
	return add(size);
}

代码解析

  • 第6,18行:若队列无效,则返回空值,即放入元素失败。
  • 第8-10,20-23行:放入元素并返回放入之前的元素数量。

取出单个元素

仅当取出元素而出口队列为空,才交换出口队列与入口队列。

template <typename _Element>
bool DoubleQueue<_Element>::pop(Element& _element)
{
	std::lock_guard lock(_exitMutex);
	if (empty()) return false;

	if (_exitQueue.empty())
	{
		std::lock_guard lock(_entryMutex);
		_exitQueue.swap(_entryQueue);
	}

	subtract(1);
	_element = std::move(_exitQueue.front());
	_exitQueue.pop_front();
	return true;
}

template <typename _Element>
auto DoubleQueue<_Element>::pop() -> std::optional<Element>
{
	std::lock_guard lock(_exitMutex);
	if (empty()) return std::nullopt;

	if (_exitQueue.empty())
	{
		std::lock_guard lock(_entryMutex);
		_exitQueue.swap(_entryQueue);
	}

	subtract(1);
	std::optional result = std::move(_exitQueue.front());
	_exitQueue.pop_front();
	return result;
}

代码解析

  • 第5,23行:若队列为空,则取出失败。
  • 第7-11,25-29行:若出口队列为空,先锁定入口互斥元,再交换出口队列与入口队列,而后解锁入口互斥元。
  • 第13,31行:减少元素数量。
  • 第14行:支持元素的完全移动语义。
  • 第15,33行:弹出队首元素。
  • 第32行:编译器RVO机制决定完全移动语义或者移动语义与复制语义。

批量取出元素

取出两个队列的所有元素,放入指定队列。

template <typename _Element>
bool DoubleQueue<_Element>::pop(QueueType& _queue)
{
	std::lock_guard exitLock(_exitMutex);
	if (empty()) return false;

	_queue.splice(_queue.cend(), _exitQueue);

	std::lock_guard entryLock(_entryMutex);
	_queue.splice(_queue.cend(), _entryQueue);
	set(_size, 0);
	return true;
}

代码解析

  • 第7行:取出出口队列所有元素,放入指定队列。
  • 第10行:取出入口队列所有元素,放入指定队列。
  • 第11行:元素数量设为零。

清空队列

注意互斥元锁定顺序与取出元素函数一致,避免因锁定顺序不同,而彼此等待释放互斥元,以致出现死锁问题。

template <typename _Element>
auto DoubleQueue<_Element>::clear() -> SizeType
{
	std::scoped_lock lock(_exitMutex, _entryMutex);
	_exitQueue.clear();
	_entryQueue.clear();
	return exchange(_size, 0);
}

代码解析

  • 第5,6行:清空出口队列与入口队列。
  • 第7行:元素数量设为零,返回操作之前的元素数量。
  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值