利用C++如何实现一个阻塞队列详解

阻塞队列是多线程中常用的数据结构,对于实现多线程之间的数据交换、同步等有很大作用。

阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。简而言之,阻塞队列是生产者用来存放元素、消费者获取元素的容器。

考虑下,这样一个多线程模型,程序有一个主线程 master 和一些 worker 线程,master 线程负责接收到数据,给 worker 线程分配数据,worker 线程取得一个任务后便可以开始工作,如果没有任务便阻塞住,节约 cpu 资源。

  • master 线程  (生产者):负责往阻塞队列中塞入数据,并唤醒正在阻塞的 worker 线程。
  • workder 线程(消费者):负责从阻塞队列中取数据,如果没有数据便阻塞,直到被 master 线程唤醒。

那么怎样的数据结构比较适合做这样的唤醒呢?显而易见,是条件变量,在 c++ 11 中,stl 已经引入了线程支持库。

C++11 中条件变量

条件变量一般与一个 互斥量 同时使用,使用时需要先给互斥量上锁,然后条件变量会检测是否满足条件,如果不满足条件便会暂时释放锁,然后阻塞线程。

c++ 11使用方法主要如下:

#include <mutex>
#include <condition_value>
// 互斥量与条件变量
std::mutex m_mutex;
std::condition_value m_condition;

// 请求信号的一方
std::unique_lock<std::mutex> lock(mutex);
while(xxx)
{
 // 这里会先释放 lock,
 // 如果有信号唤醒的话,会重新加锁。
 m_condition.wait(lock);
}

// 发送消息进行同步的一方
{
 std::unique_lock<std::mutex> lock(mutex);
 // 唤醒其他正在 wait 的线程
 m_condition.notify_all();
}

用 C++11 实现阻塞队列

我们使用条件变量包装 STL 中的 queue 就可以实现阻塞队列功能,如果有兴趣,甚至可以自己实现一个效率更高的队列数据结构。

我们先假设一下阻塞队列需要如下接口:

  • push 将一个变量塞入队列;
  • take 从队列中取出一个元素;
  • size 查看队列有多少个元素;
template <typename T>
class BlockingQueue
{
public:
 BlockingQueue();
 void push(T&& value);
 T take();
 size_t size() const;

private:
 // 实际使用的数据结构队列
 std::queue<T> m_data;

 // 条件变量
 std::mutex m_mutex;
 std::condition_variable m_condition;
};

push 一个变量时,我们需要先加锁,加锁成功后才可以压入变量,这是为了线程安全。压入变量后,就可以发送信号通知正在阻塞的条件变量。

 void push(T&& value)
 {
  // 往队列中塞数据前要先加锁
  std::unique_lock<std::mutex> lock(m_mutex);
  m_data.push(value);
  // 唤醒正在阻塞的条件变量
  m_condition.notify_all();
 }

take 一个变量时,就要有些不一样:

  1. 先加锁,加锁成功后,如果队列不为空,可以直接取数据,不需要 wait;
  2. 如果队列为空呢,则 wait 等待,直到被唤醒;
  3. 考虑特殊情况,唤醒后队列依然是空的……
 T take()
 {
  std::unique_lock<std::mutex> lock(m_mutex);
  while(m_data.empty())
  {
   // 等待
   m_condition.wait(lock);
  }
  assert(!m_data.empty());
  T value(std::move(m_data.front()));
  m_data.pop();

  return value;
 }

总结下,代码如下:

#ifndef BLOCKINGQUEUE_H
#define BLOCKINGQUEUE_H

#include <queue>
#include <mutex>
#include <condition_variable>
#include <assert.h>

template <typename T>
class BlockingQueue
{
public:
 BlockingQueue()
  :m_mutex(),
   m_condition(),
   m_data()
 {
 }

 // 禁止拷贝构造
 BlockingQueue(BlockingQueue&) = delete;

 ~BlockingQueue()
 {
 }

 void push(T&& value)
 {
  // 往队列中塞数据前要先加锁
  std::unique_lock<std::mutex> lock(m_mutex);
  m_data.push(value);
  m_condition.notify_all();
 }

 void push(const T& value)
 {
  std::unique_lock<std::mutex> lock(m_mutex);
  m_data.push(value);
  m_condition.notify_all();
 }

 T take()
 {
  std::unique_lock<std::mutex> lock(m_mutex);
  while(m_data.empty())
  {
   m_condition.wait(lock);
  }
  assert(!m_data.empty());
  T value(std::move(m_data.front()));
  m_data.pop();

  return value;
 }

 size_t size() const
 {
  std::unique_lock<std::mutex> lock(m_mutex);
  return m_data.size();
 }
private:
 // 实际使用的数据结构队列
 std::queue<T> m_data;

 // 条件变量的锁
 std::mutex m_mutex;
 std::condition_variable m_condition;
};
#endif // BLOCKINGQUEUE_H

实验代码

我们写个简单的程序实验一下,下面程序有 一个 master 线程,5个 worker 线程,master线程生成一个随机数,求 0-随机数 的和。

#include <iostream>
#include <thread>
#include <mutex>
#include <random>

#include <windows.h>

#include <blockingqueue.h>
using namespace std;

int task=100;
BlockingQueue<int> blockingqueue;
std::mutex mutex_cout;

void worker()
{
 int value;
 thread::id this_id = this_thread::get_id();
 while(true)
 {
  value = blockingqueue.take();
  uint64_t sum = 0;
  for(int i = 0; i < value; i++)
  {
   sum += i;
  }

  // 模拟耗时操作
  Sleep(100);

  std::lock_guard<mutex> lock(mutex_cout);
  std::cout << "workder: " << this_id << " "
     << __FUNCTION__
     << " line: " << __LINE__
     << " sum: " << sum
     << std::endl;
 }
}

void master()
{
 srand(time(nullptr));
 for(int i = 0; i < task; i++)
 {
  blockingqueue.push(rand()%10000);
  printf("%s %d %i\n",__FUNCTION__, __LINE__, i);
  Sleep(20);
 }
}

int main()
{
 thread th_master(master);
 std::vector<thread> th_workers;
 for(int i =0; i < 5; i++)
 {
  th_workers.emplace_back(thread(worker));
 }

 th_master.join();
 return 0;
}

从输出结果可以看出,master 线程将任务分配给了正在空闲的 worker 线程,具体是哪个线程就看操作系统的随机调度了。

master 46 5
worker: 3 worker line: 34 sum: 20998440
master 46 6
worker: 7 worker line: 34 sum: 3308878
master 46 7
worker: 4 worker line: 34 sum: 34598721
master 46 8
worker: 6 worker line: 34 sum: 1563796
master 46 9
worker: 5 worker line: 34 sum: 27978940

Reference

条件变量

总结

到此这篇关于利用C++如何实现一个阻塞队列的文章就介绍到这了,更多相关C++实现阻塞队列内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Linux C++ 使用condition实现阻塞队列的方法

    实例如下: /* * BlockingQueue.h * * Created on: 2014年6月10日 * Author: */ #ifndef BLOCKINGQUEUE_H_ #define BLOCKINGQUEUE_H_ #include <iostream> #include <pthread.h> using namespace std; //template <typename T > class BlockingQueue { public: Blo

  • 利用C++如何实现一个阻塞队列详解

    阻塞队列是多线程中常用的数据结构,对于实现多线程之间的数据交换.同步等有很大作用. 阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程.简而言之,阻塞队列是生产者用来存放元素.消费者获取元素的容器. 考虑下,这样一个多线程模型,程序有一个主线程 master 和一些 worker 线程,master 线程负责接收到数据,给 worker 线程分配数据,worker 线程取得一个任务后便可以开始工作,如果没有任务便阻塞住,节约 cpu 资源. mast

  • Java 阻塞队列详解及简单使用

     Java 阻塞队列详解 概要: 在新增的Concurrent包中,BlockingQueue很好的解决了多线程中,如何高效安全"传输"数据的问题.通过这些高效并且线程安全的队列类,为我们快速搭建高质量的多线程程序带来极大的便利.本文详细介绍了BlockingQueue家庭中的所有成员,包括他们各自的功能以及常见使用场景. 认识BlockingQueue阻塞队列,顾名思义,首先它是一个队列,而一个队列在数据结构中所起的作用大致如下图所示: 从上图我们可以很清楚看到,通过一个共享的队列,

  • Java多线程案例之阻塞队列详解

    目录 一.阻塞队列介绍 1.1阻塞队列特性 1.2阻塞队列的优点 二.生产者消费者模型 2.1阻塞队列对生产者的优化 三.标准库中的阻塞队列 3.1Java提供阻塞队列实现的标准类 3.2Blockingqueue基本使用 四.阻塞队列实现 4.1阻塞队列的代码实现 4.2阻塞队列搭配生产者与消费者的代码实现 一.阻塞队列介绍 1.1阻塞队列特性 阻塞队列特性: 一.安全性 二.产生阻塞效果 阻塞队列是一种特殊的队列. 也遵守 “先进先出” 的原则.阻塞队列能是一种线程安全的数据结构, 并且具有

  • Java并发编程之阻塞队列详解

    1.什么是阻塞队列? 队列是一种数据结构,它有两个基本操作:在队列尾部加入一个元素,从队列头部移除一个元素.阻塞队里与普通的队列的区别在于,普通队列不会对当前线程产生阻塞,在面对类似消费者-生产者模型时,就必须额外的实现同步策略以及线程间唤醒策略.使用阻塞队列,就会对当前线程产生阻塞,当队列是空时,从队列中获取元素的操作将会被阻塞,当队列是满时,往队列里添加元素的操作也会被阻塞. 2.主要的阻塞队列及其方法 java.util.concurrent包下提供主要的几种阻塞队列,主要有以下几个: 1

  • 如何从零开始利用js手写一个Promise库详解

    前言 ECMAScript 是 JavaScript 语言的国际标准,JavaScript 是 ECMAScript 的实现.ES6 的目标,是使得 JavaScript 语言可以用来编写大型的复杂的应用程序,成为企业级开发语言. 概念 ES6 原生提供了 Promise 对象. 所谓 Promise,就是一个对象,用来传递异步操作的消息.它代表了某个未来才会知道结果的事件(通常是一个异步操作),并且这个事件提供统一的 API,可供进一步处理. 三道思考题 刚开始写前端的时候,处理异步请求经常用

  • SpringBoot利用AOP实现一个日志管理详解

    目录 1. 需求 2. 新建一张日志表 3. 写相应的Controller层 4.Service接口层 5.Service实现 6.Mapper接口 7.Mapper.xml(我用的是Mybatis) 8.CspLog 9.实体类SysOperCspLog 10. 定义日志管理的切面 11.AsyncFactoryCsp 12. 写一个Controller的Demo来执行一条日志试试 1. 需求 目前有这么个问题,有两个系统CSP和OMS,这俩系统共用的是同一套日志操作:Log;目前想区分下这俩

  • Bloc事件流是一个阻塞队列结论解析

    前言 在Flutter中有state的概念,我们使用Bloc进行状态管理,通过Bloc.addEvent的方式进行事件传递,状态变更.关于Bloc的基础用法,可以查阅Bloc官网相关资料,这里我们仅记录一下Bloc的队列等待. 新建一个Bloc类 class TestBloc extends Bloc<TestEvent, TestState> { TestBloc() : super(new TestState()); @override Stream<TestState> ma

  • RocketMq 消息重试机制及死信队列详解

    目录 生产者消息重试 消费者消息重试 并发消费 顺序消费 并发消费和顺序消费区别 死信队列 实践出真知 公共部分创建 测试并发消费 并发消费状态 测试顺序消费 顺序消费状态 测试死信队列 死信队列特性 生产者消息重试 消息队列中的消息消费时并不能保证总是成功的,那失败的消息该怎么进行消息补偿呢?这就用到今天的主角消息重试和死信队列了. 有时因为网路等原因生产者也可能发送消息失败,也会进行消息重试,生产者消息重试比较简单,在springboot中只要在配置文件中配置一下就可以了. # 异步消息发送

  • Java利用StampedLock实现读写锁的方法详解

    目录 概述 StampedLock介绍 演示例子 性能对比 总结 概述 想到读写锁,大家第一时间想到的可能是ReentrantReadWriteLock.实际上,在jdk8以后,java提供了一个性能更优越的读写锁并发类StampedLock,该类的设计初衷是作为一个内部工具类,用于辅助开发其它线程安全组件,用得好,该类可以提升系统性能,用不好,容易产生死锁和其它莫名其妙的问题.本文主要和大家一起学习下StampedLock的功能和使用. StampedLock介绍 StampedLock的状态

  • Python数据结构之队列详解

    目录 0. 学习目标 1. 队列的基本概念 1.1 队列的基本概念 1.2 队列抽象数据类型 1.3 队列的应用场景 2. 队列的实现 2.1 顺序队列的实现 2.2 链队列的实现 2.3 队列的不同实现对比 3. 队列应用 3.1 顺序队列的应用 3.2 链队列的应用 3.3 利用队列基本操作实现复杂算法 0. 学习目标 栈和队列是在程序设计中常见的数据类型,从数据结构的角度来讲,栈和队列也是线性表,是操作受限的线性表,它们的基本操作是线性表操作的子集,但从数据类型的角度来讲,它们与线性表又有

随机推荐