最近在系统学习C++ 并发编程,便想着开启C++ 并发编程这一系列的博客,记录所遇到的,觉得需要记录下来的知识点。 那么第一篇开始记录一下如何设计一个支持并发的阻塞队列(BlockQueue)
1 阻塞队列的定义
首先阻塞队列肯定是一个队列,是一种先进先出的数据结果,与一般的数据结构相比,多了阻塞两个字,对应的与普通队列不同的点如下:
- 当队列空时,从队列中取出元素会阻塞(先等着,直到队列中有元素为止)
- 当队列满时,往队列中增加元素会阻塞(队列有容量限制,不能无节制的往队列中增加元素) 阻塞队列常用来实现生产者消费者模型。
2 阻塞队列的C++实现
2.1 成员变量
用stl的queue容器来存放实际数据。结合mutex和condition变量来支持并发。
size_t capacity_;
std::queue<T> queue_;
std::mutex mutex_;
std::condition_variable cond_;
2.2 Push接口设计
此接口实现的是往阻塞队列尾部插入一个元素。当队列满时,插入动作阻塞中,直到队列中元素个数小于阻塞队列容量。
-
- 基于
RAII的思想锁住mutex;
- 基于
-
- 判断queue的size是否大于等于设定的capacity。当大于时,队列被阻塞,循环等待条件变量;
-
- 使用queue的原生push接口push数据。
实现代码如下:
void Push(const T &value)
{
std::unique_lock<std::mutex> lock(mutex_);
while (queue_.size() >= capacity_) {
cond_.wait(lock);
}
assert(queue_.size() < capacity_);
queue_.push(value);
cond_.notify_one();
}
2.3 Take接口的实现
此接口是从阻塞队列头部取出一个元素。当队列为空时,阻塞等待,直到队列中有元素为止。
-
- 基于
RAII的思想锁住mutex;
- 基于
-
- 判断queue的size是否为空,为空时队列被阻塞,循环等待条件变量;
-
- 使用queue的原生pop接口获取头部一个元素。
实现代码如下:
T Take()
{
std::unique_lock<std::mutex> lock(mutex_);
while (queue_.size() == 0) {
cond_.wait(lock);
}
assert(queue_.size() > 0);
T value(std::move(queue_.front()));
queue_.pop();
cond_.notify_one();
return value;
}
2.4 Size接口的实现
此接口是获取queue的实时大小,需要注意并发访问。
-
- 基于
RAII的思想锁住mutex;
- 基于
-
- 使用queue的原生size接口获取队列的大小。
实现代码如下:
size_t Size()
{
std::unique_lock<std::mutex> lock(mutex_);
return queue_.size();
}
2.5 完整实现代码
#ifndef BLOCKQUEUE_H_
#define BLOCKQUEUE_H_
#include <queue>
#include <mutex>
#include <condition_variable>
#include <cassert>
template <typename T>
class BlockQueue
{
public:
BlockQueue(size_t capacity)
: capacity_(capacity),
queue_(),
mutex_(),
cond_()
{}
void Push(T &&value)
{
std::unique_lock<std::mutex> lock(mutex_);
while (queue_.size() >= capacity_) {
cond_.wait(lock);
}
assert(queue_.size() < capacity_);
queue_.push(value);
cond_.notify_one();
}
void Push(const T &value)
{
std::unique_lock<std::mutex> lock(mutex_);
while (queue_.size() >= capacity_) {
cond_.wait(lock);
}
assert(queue_.size() < capacity_);
queue_.push(value);
cond_.notify_one();
}
T Take()
{
std::unique_lock<std::mutex> lock(mutex_);
while (queue_.size() == 0) {
cond_.wait(lock);
}
assert(queue_.size() > 0);
T value(std::move(queue_.front()));
queue_.pop();
cond_.notify_one();
return value;
}
size_t Size()
{
std::unique_lock<std::mutex> lock(mutex_);
return queue_.size();
}
private:
size_t capacity_;
std::queue<T> queue_;
std::mutex mutex_;
std::condition_variable cond_;
};
#endif
3 基于阻塞队列实现一个生产者消费者模型
生产者消费者问题是多线程同步的经典问题,生产者和消费者共享固定大小的缓冲区,生产者产生数据放入到缓冲区中,消费者从缓冲区中取出数据消耗掉。
本文实现一个简单的生产者消费者模型。共享缓冲区即采用前文设计的阻塞队列,生产者往队列中填入一个int数据,消费者取出队列中的头部数据。
3.1 生产者函数实现
实现代码如下:
void Producer()
{
std::thread::id id = std::this_thread::get_id();
while (true) {
int value = rand();
block_queue.Push(value);
{
std::lock_guard<std::mutex> lock(out_mutex);
std::cout << "Thread id : " << id << " produces value : " << value << std::endl;
}
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
3.2 消费者函数实现
实现代码如下:
void Consumer()
{
std::thread::id id = std::this_thread::get_id();
while (true) {
int value = block_queue.Take();
{
std::lock_guard<std::mutex> lock(out_mutex);
std::cout << "Thread id : " << id << " consumes value : " << value << std::endl;
}
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
}
3.3 main函数以及运行效果
main函数如下:
int main()
{
int producer_cnt = 2;
int consumer_cnt = 5;
std::vector<std::thread> producer_vec;
std::vector<std::thread> consumer_vec;
for (int i = 0; i < producer_cnt; ++i) {
producer_vec.emplace_back(std::thread(Producer));
}
for (int i = 0; i < consumer_cnt; ++i) {
consumer_vec.emplace_back(std::thread(Consumer));
}
for (auto &t : producer_vec) {
t.join();
}
for (auto &t : consumer_vec) {
t.join();
}
return 0;
}
输出结果如下:
Thread id : 0x16fc7f000 produces value : 282475249
Thread id : 0x16fbf3000 produces value : 16807
Thread id : 0x16feaf000 consumes value : 282475249
Thread id : 0x16fd97000 consumes value : 16807
Thread id : 0x16fbf3000 produces value : 984943658
Thread id : 0x16fe23000 consumes value : 984943658
Thread id : 0x16fc7f000 produces value : 1622650073
Thread id : 0x16fd0b000 consumes value : 1622650073
Thread id : 0x16fbf3000 produces value : 1144108930
注意
在调试输出的时候我发现有的时候会先print消费了x值,然后print出来生产了x值。造成这样的原因是多线程运行时,假设线程1是生产者线程,生产出x值,线程2是消费者,消费x值。线程1完成了往阻塞队列中push这一动作,但是,在没有获取到out_mutex之前,线程2已经消费了x值,并先获取到了out_mutex,那么就会先print出线程2消费x的log,然后才是线程1生产的log。
| 线程1 线程2
| block_queue.Push(value);
| int value = block_queue.Take();
| std::lock_guard<std::mutex> lock(out_mutex);
| std::lock_guard<std::mutex> lock(out_mutex);
V
线程并发过程
总之,生产者消费者消费数据的相对顺序是正确的(先生产的先消费),且队列中的元素数量始终小于容量,则该模型是正确的。
4 完整代码
完整代码链接如下,欢迎大家fork/star, 后续会持续更新C++并发编程的相关内容。 C++并发编程实例