目录
一、业务场景:
网络线程负责接收数据,业务线程负责处理数据
二、使用std::list伪代码实现
std::list<int> g_msgQue;
std::mutex g_mutext;
生产者:
for(int i = 0; i < size; ++i)
{
std::lock_guard<std::mutex> locker(g_mutext);
g_msgQue.push_back(i);
}
消费者:
std::list<int> tmp;
while(!g_msgList.empty())
{
{
// 使用中括号减少锁生命周期
std::lock_guard<std::mutex> locker(g_mutext);
tmp.splice(tmp.end(), g_msgQue);
}
while(!tmp.empty())
{
int data = tmp.front();
tmp.pop_front()
// TODO something
}
}
优点:
1、使用锁不管多少个生产者都能保证数据准确性;
2、编码简单,熟悉list以及锁的使用即可;
缺点:
使用锁必然会有性能损耗;
三、单生产单消费队列
设计思路
1、设计内容缓存,用于存储数据;同时为了防止在运行过程中产生内存动态调整,以及调整内存过程中多线程可能出现的异常,考虑提前分配固定大小的内存;
2、设计写入指针,写入数据时,指针自增(对应我们生产者线程);
3、设计读取指针,取出数据时,指针自增(对应我们消费者线程);
4、设计写入和读取指针正向递增,使用掩码方式取读写位置,对容量大小取2的N次幂;
5、考虑使用模板实现,以满足不同数据类型的需要;
编码实现
1、设计成员变量
首先给我们的队列取个名字,假定为sc_sp_queue,且包含以下成员变量:
template<class _Ty, size_t _POW>
class sc_sp_queue
{
using val_type = _Ty;
using val_ptr = *_Ty;
private:
uint64_t _write; // 写指针
uint64_t _read; // 读指针
size_t _capacity; // 数据容量
size_t _mask; // 取模掩码
val_ptr _data; // 数据内容
};
2、设计成员函数
程序函数需满足基本的读写要求,也要满足常用的查询功能,提供如下接口:
template<class _Ty, size_t _POW>
class sc_sp_queue
{
using val_type = _Ty;
using val_ptr = *_Ty;
public:
// 从尾部插入数
bool push_back(const val_type & __val)
{
if (_write < _read + _capacity)
{
// placement new with copy constructor
new ((void *)&_data[_write & _mask]) val_type(__val);
++_write;
return true;
}
return false;
}
// 从头部取数据
val_ptr front()
{
if (_read < _write)
{
return &_data[_read & _mask];
}
}
// 释放头部数据
void pop_front()
{
if (_read < _write)
{
val_ptr p = &_data[_read & _mask];
p->~val_type();
++_read;
}
}
// 获取容量
size_t capacity()
{
return _capacity;
}
// 获取已使用数据
size_t size()
{
return _write - _read;
}
// 判断是否为空
bool empty()
{
return size() == 0;
}
};
3、存在的问题
考虑以下测试程序,会不会打印23行的结果?
#include <thread>
#include <iostream>
size_t _read = 0;
size_t _write = 0;
int main()
{
// 生产者线程
std::thread([&]()->void {
std::this_thread::sleep_for(std::chrono::seconds(1));
while (true) {
++_write;
}
}).detach();
// 消费者线程
while (true)
{
if (_read < _write)
{
++_read;
std::cout << "read=" << _read << std::endl;
}
}
return 0;
}
当我们编译时开启优化(VS编译使用Release,g++编译开启O2或O3),很显然并不会打印23行的结果,这是因为变量_write已经被写入寄存器,而且我们一直是死循环读取变量_write,所以寄存器不会被更新;如果给变量_write加上关键字volatile结果又会如何,有条件的小伙伴可以试一试。
volatile的使用网上有很多介绍,这里表示每次读取变量_write,都会从内存读取数据,而不会使用寄存器缓存。这当然也使编译器失去对变量_write优化的机会,会损失部分性能。
聪明的小伙伴此时一定会想到我想表达的问题,即sc_sp_queue的_write和_read同样需要volatile修饰。
3、完善成员函数
调用成员函数push_back时候,使用的是拷贝构造函数。从c++11开始,提供了不定参数模板和完美转发,这允许我们可以直接以自定义构造函数的方式直接在堆上构造对象。所以我们可以提供如下函数以提供性能更高的接口:
template <class... Args>
bool emplace_back(Args &&... __args)
{
if (_write < _read + _capacity)
{
// placement new with custom-constructor
new ((void*)&_data[_write & _mask]) val_type(std::forward<Args>(__args)...);
++_write;
return true;
}
return false;
}
4、完整代码实现
#include <cstdint>
#include <cstdlib>
#include < utility >
template<class _Ty, size_t _POW>
class sc_sp_queue
{
using val_type = _Ty;
using val_ptr = _Ty*;
const uint16_t MAX_POW = 32;
public:
sc_sp_queue()
{
uint16_t pow = _POW;
if (pow > MAX_POW)
{
pow = MAX_POW;
}
_capacity = ((uint64_t)1 << pow);
_data = (val_ptr*)::malloc(_capacity * sizeof(val_type));
_mask = _capacity - 1;
_read = 0;
_write = 0;
}
~sc_sp_queue()
{
_read = 0;
_write = 0;
::free(_data), _data = nullptr;
}
// 从尾部插入数
bool push_back(const val_type& __val)
{
if (_write < _read + _capacity)
{
// placement new with copy constructor
new ((void*)&_data[_write & _mask]) val_type(__val);
++_write;
return true;
}
return false;
}
template <class... _Args>
bool emplace_back(_Args &&...__args)
{
if (_write < _read + _capacity)
{
// placement new with custom-constructor
new ((void*)&_data[_write & _mask]) val_type(std::forward<_Args>(__args)...);
++_write;
return true;
}
return false;
}
// 从头部取数据
val_ptr front()
{
if (_read < _write)
{
return &_data[_read & _mask];
}
}
// 释放头部数据
void pop_front()
{
if (_read < _write)
{
val_ptr p = &_data[_read & _mask];
p->~val_type();
++_read;
}
}
// 获取容量
size_t capacity()
{
return _capacity;
}
// 获取已使用数据
size_t size()
{
return _write - _read;
}
// 判断是否为空
bool empty()
{
return size() == 0;
}
private:
volatile uint64_t _write; // 写指针
volatile uint64_t _read; // 读指针
size_t _capacity; // 数据容量
size_t _mask; // 取模掩码
val_ptr _data; // 数据内容
};