双缓冲技术优化任务队列
设计并实现轻量级高并发线程池,命名为eterfree::ThreadPool,简称为Eterfree线程池。
前序
初衷
Eterfree线程池以一个基于Boost程序库的线程池(简称为参照线程池)为设计目标,希望调度线程执行任务的效率超过参照线程池。
困境
梦想总是美好的,而现实总是残酷的。
Eterfree线程池的初始性能并未优于参照线程池。
突破
无论正在经历什么,都不要轻易放弃,因为从没有一种坚持会被辜负。
初步测试,向任务队列放入任务之时,Eterfree线程池更耗时。经过分析,放入和取出任务操作互斥,频繁放入任务会影响线程获取任务,从而降低线程池性能。
因此决定优化任务队列,封装双缓冲队列类模板。仅当取出元素而出口队列为空时,访问两个队列才相互影响,似此出入队列得以提高效率。
结果
然而,战胜他人无用,须得超越自己。
通过引入双缓冲队列,提升线程池调度效率。最终,Eterfree线程池与参照线程池在伯仲之间,或许在性能上略优于参照线程池。
文件依赖性
双缓冲队列类模板不依赖标准库以外的文件,定义于头文件DoubleQueue.hpp。
DoubleQueue.hpp
- GitCode: DoubleQueue.hpp
- Gitee: DoubleQueue.hpp
- GitHub: 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();
};
成员变量
双缓冲队列类模板的成员变量如表所示:
变量名 | 类型 | 说明 |
---|---|---|
_capacity | AtomicType | 队列容量 |
_size | AtomicType | 元素数量 |
_entryMutex | mutable MutexType | 入口互斥元 |
_entryQueue | QueueType | 入口队列 |
_exitMutex | mutable MutexType | 出口互斥元 |
_exitQueue | QueueType | 出口队列 |
双缓冲队列分为入口队列与出口队列。在访问两个队列之时,分别采用入口互斥元和出口互斥元,旨在灵活地控制互斥粒度。
元素数量采用原子计数,在多线程环境,以最小互斥粒度确保元素数量准确无误。
成员函数
完全复制
复制双缓冲队列的可复制成员变量,包括出口队列与入口队列,元素数量和队列容量。
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行:元素数量设为零,返回操作之前的元素数量。