介绍
我们经常需要在多线程间通信,例如网络通信线程和逻辑线程,网络线程需要把收到的数据 传递到 逻辑线程进行处理;同样 逻辑线程 需要把发送的数据,传递到网络线程进行发送。 这时我们就需要一种数据结构 同步队列。
由于C++11 对线程提供了支持,我们需要一种支持 先入先出的数据结构即可 ,STL库里面已经有现成的 std::deuqe, std::queue。但C++11 引入了右值引用,类的成员函数添加了移动构造函数,利用这个特性让 std::vector在某些操作情况下可能性能更佳。
同步队列的操作和普通的一样:队尾插入,队头出队。
入队
为了和stl里面的容器操作接口保持一致,入队函数如下
void push_back(const T& x)
{
std::unique_lock<std::mutex> lck(_mutex);
_queue.push_back(x);
}
这个函数很简单,就是对容器加锁,然后插入数据,防止多个线程同时对队列操作。
C++11 中许多STL容器的插入操作 引入了一个新的函数 emplace_back/emplace, 这个同样是和右值引用相关,如果插入的数据是右值,那就会调用这个数据的移动构造函数,而不是拷贝构造函数,这样就会比 push_back() 少一次拷贝。同样我们也实现这个操作:
template<typename _Tdata>
void emplace_back(_Tdata&& v)
{
std::unique_lock<std::mutex> lck(_mutex);
_queue.emplace_back(std::forward<_Tdata>(v));
}
和push_back 一样,先加锁,再操作容器。_Tdata 是一个未定的引用类型,可以是右值或者左值,由具体传入的参数确定(详见C++11相关资料)。由于这个模板函数被调用后就已经实例化,Tdata 将具有确定的类型,在函数内部将会变为左值,std::forward 被称为完美转发,将会保持参数的原有类型,传递给另一个函数。这样我们就可以把右值引用类型参数传递给 容器的 emplace_back 函数。
出队
出队函数入下
T pop_front()
{
std::unique_lock<std::mutex> lck(_mutex);
if(_queue.empty())
{
return T();
}
assert(!_queue.empty());
T t(_queue.front());
_queue.pop_front();
return t;
}
先加锁,如果队列为空,则返回一个默认的对象。不为空则弹出队首数据。下面将会提供一个获取队列长度的函数。 使用的时候应该 先检查长度 再出队操作。
获取队列长度
size_t size()
{
std::unique_lock<std::mutex> lck(_mutex);
return _queue.size();
}
返回容器数据个数即可。
Move操作
一般情况下 我们是 边入队,边出队,由于每个操作都是对队列中的一个元素的操作,可能更加频繁的加锁解锁。使用std::move 返回容器的右值引用对象,这样可以获取容器中的所有元素,并且清空容器,这是一个批量操作,比单个元素操作更高效。
std::deque<T> move()
{
std::unique_lock<std::mutex> lck(_mutex);
auto tmp = std::move(_queue);
m_notFull.notify_one();
return std::move(tmp);
}
实现代码
#pragma once
#include <mutex>
#include <condition_variable>
#include <cassert>
#include <type_traits>
#include <atomic>
namespace moon
{
template<typename T, typename TContainer = std::deque<T> , size_t max_size = 50>
class sync_queue
{
public:
sync_queue()
:m_exit(false)
{
}
sync_queue(const sync_queue& t) = delete;
sync_queue& operator=(const sync_queue& t) = delete;
void push_back(const T& x)
{
std::unique_lock<std::mutex> lck(m_mutex);
m_notFull.wait(lck, [this] {return m_exit || (m_queue.size() < max_size); });
m_queue.push_back(x);
}
template<typename _Tdata>
void emplace_back(_Tdata&& v)
{
std::unique_lock<std::mutex> lck(m_mutex);
m_notFull.wait(lck, [this] {return m_exit || (m_queue.size() < max_size); });
m_queue.emplace_back(std::forward<_Tdata>(v));
}
size_t size()
{
std::unique_lock<std::mutex> lck(m_mutex);
return m_queue.size();
}
//替代pop_front
TContainer move()
{
std::unique_lock<std::mutex> lck(m_mutex);
auto tmp = std::move(m_queue);
m_notFull.notify_one();
return std::move(tmp);
}
//当程序退出时调用此函数,触发条件变量
void exit()
{
m_exit = true;
}
private:
std::mutex m_mutex;
std::condition_variable m_notFull;
TContainer m_queue;
std::atomic_bool m_exit;
};
}
TContainer支持 std::vector,std::deque. 这里取消了pop_front 操作,因为 std::vector 没有pop_front, 为了统一 使用 move函数。可以给队列限制大小,防止一直入队,占用太多内存。
示例
这个示例演示了使用同步队列进行 异步 加法计算
#include <thread>
#include "sync_queue.h"
struct SAddContext
{
SAddContext()
:a(0),b(b)
{
}
int a;
int b;
};
struct SAddResult
{
SAddResult()
:a(0), b(0),result(0)
{
}
int a;
int b;
int result;
};
int main()
{
moon::sync_queue<SAddContext> que1;//main thread - calculate thread
moon::sync_queue<SAddResult> que2;//calculate thread - print thread
std::thread calculate([&que1,&que2]() {
while (1)
{
//如果队列为空 ,等待
if (que1.size() == 0)
{
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
//获取所有异步计算请求
auto data = que1.move();
for (auto& dat : data)
{
SAddResult sr;
sr.a = dat.a;
sr.b = dat.b;
sr.result = dat.a + dat.b;
que2.push_back(sr);
}
}
});
std::thread printThread([&que2]() {
while (1)
{
//如果队列为空 ,等待
if (que2.size() == 0)
{
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
auto data = que2.move();
for (auto& dat : data)
{
printf("%d + %d = %d\r\n", dat.a, dat.b, dat.result);
}
}
});
int x = 0;
int y = 0;
while (std::cin >> x >> y)
{
SAddContext sc;
sc.a = x;
sc.b = y;
que1.push_back(sc);
}
};