生产者消费者队列
它是实现线程间协作,交互一种重要手段。从一端放数据,从另一端取数据。放入数据的线程称为生产者,取出数据的线程称为消费者。生产者和消费者可以有一个或多个。
-
生产者,消费者线程间通过条件变量来实现协作
-
对队列的访问需要加锁互斥
用途划分:
根据队列的用途来划分为两大类
- 数据分发
队列中存放的业务数据。分别有一个或多个生产者,消费者线程。生产者线程产生不同类型的数据,通过队列分发给不同消费者线程
- 任务队列
队列中存放的是可调用对象,分别有一个或多个生产者,消费者线程
- 在调用要求时序的情况下,应该只有一个生产者和一个消费者线程,一个队列,时序的要求由队列的先进先出的特性保证
- 在调用不要求时序的情况下,则可以有多个生产者,消费者线程
通过任务队列,来实现异步调用。发起业务操作的线程不会被阻塞,业务执行函数被放到另外一个线程执行,比如释放过程耗时高的资源且释放操作不能并发进行时。可以将释放操作专门放到一个线程中去做,此时可以有多个生产者线程,一个消费者线程,生产者将需要释放的资源对象放入队列,消费者线程依次取出后进行释放操作。此时就需要队列来实现异步操作
容量划分:
根据队列的容量划分为两大类
- 有界队列
容量有大小限制,当满了后,生产者线程需要等待,为空时,消费者线程需要等待。这种队列可以用于数据分发的场景,但不能用于异步调用(异步调用的特性是生产者调用能马上返回,所以如果生产者阻塞在等待容器空闲显然是不满足要求的)
- 无界队列
容量大小无限制,生产者线程可以一直放数据,队列为空的时候,消费者线程需要等待。这种队列即可以用于数据分发,也可以用于异步操作。这种情况下,消费者线程数量要大于生产者线程,是让数据或是任务及时被处理,避免堆积。
-
在用途这个方向的划分,对于队列的实现,没有太大区别。只是队列中放的数据类型不一样。一种是业务的数据,一种是可调用的对象。
-
那么有界队列和无界队列,在队列的实现方式上是有很大的不同。
实现
有界队列
-
容量有限,对生产者线程来说,是需要等待队列中有空位,如果没有则应该等待。对消费者来说,是需要队列中有数据,如果没有也应该等待
-
那么生产者放入数据后,应该通知消费者。消费者取走数据后应该通知生产者有空位
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <iostream>
template<typename T>
class CBoundedQueue
{
public:
CBoundedQueue(int maxSize):m_iMaxSize(maxSize),m_iCnt(0)
{
}
~CBoundedQueue()
{
}
void Put(const T& x)
{
std::unique_lock<std::mutex> lock(m_mutex);
//有一个判断谓词,等价于while(!Pred){m_notFull.wait()}
m_notFull.wait(lock, std::bind(&CBoundedQueue::CanPut,this));
m_deqDatas.push_back(x);
++m_iCnt;
if (m_iCnt > m_iMaxSize)
{
m_iCnt = m_iMaxSize;
}
//手动释放锁,减小锁的范围
lock.unlock();
//m_notEmpty.notify_one();
m_notEmpty.notify_all();
}
T Get()
{
std::unique_lock<std::mutex> lock(m_mutex);
//有一个判断谓词,等价于while(!Pred){m_notEmpty.wait()}
m_notEmpty.wait(lock, std::bind(&CBoundedQueue::CanGet,this));
T front(m_deqDatas.front());
m_deqDatas.pop_front();
--m_iCnt;
//手动释放锁,减小锁的范围
lock.unlock();
//m_notFull.notify_one();
m_notFull.notify_all();
return front;
}
bool Empty() const
{
std::lock_guard<std::mutex> lock;
return m_deqDatas.empty();
}
bool Full() const
{
std::lock_guard<std::mutex> lock;
return m_iCnt == m_iMaxSize;
}
private:
//判断谓词
bool CanPut() { return m_iCnt < m_iMaxSize; }
bool CanGet() { return m_iCnt > 0;}
private:
//计数
int m_iCnt;
int m_iMaxSize;
//容器类型为deque
std::deque<T> m_deqDatas;
//互斥量,对队列进行同步保护
std::mutex m_mutex;
//用于限制生产者线程
std::condition_variable m_notFull;
//用于限制消费者线程
std::condition_variable m_notEmpty;
};
main函数
void consumer(CBoundedQueue<int>* dep)
{
auto thread_id = std::this_thread::get_id();
std::this_thread::sleep_for(std::chrono::seconds(3));
while (1)
{
std::cout << "thread id "<<thread_id<<","<<dep->Get() << std::endl;
}
}
int main()
{
CBoundedQueue<int> dep(10);
std::future<void> ret1 = std::async(std::launch::async, consumer, &dep);
再产生一个消费者线程
//std::future<void> ret2 = std::async(std::launch::async, consumer, &dep);
for (int i = 0; i < 12; ++i)
{
dep.Put(i);
std::cout << "put data " << i << std::endl;
}
ret1.wait();
//ret2.wait();
}
- 有界队列,必须在容器的容量上同时限制生产者和消费者线程,所以有两个信号量m_notFull,m_notEmpty。
- m_mutex 互斥量即是用于线程间同步的,包括生产者之间,消费者之间,生产者与消费者之间,也是保护条件变量。
- 当队列满时,生产者线程阻塞在m_notFull上,消费者线程阻塞在m_notEmpty上。
- m_notFull的语义是当消费者线程取出数据时,唤醒生产者线程有空位去放数据。
- m_notEmpty的语义是当生产者线程放入一个数据时,唤醒消费者线程取出数据。
无界队列
#include <thread>
#include <condition_variable>
#include <mutex>
#include <deque>
#include <iostream>
#include <future>
template<typename T>
class UnBoundedQueue
{
public:
UnBoundedQueue()
{
}
void Put(const T&x)
{
//这个lock用于对生产者间的同步
{
std::lock_guard<std::mutex> lock(m_mutex);
m_deqDatas.push_back(x);
}
//信号量用于生产者,消费者间的同步
//m_notEmpty.notify_one();
m_notEmpty.notify_all();
}
T Get()
{
//这个lock用于配合信号量的使用,
std::unique_lock<std::mutex> lock(m_mutex);
/*当后面的lambda返回false时,则信号量进入wait,此时自动释放lock
*等待到信号量后,则再获取lock
*/
m_notEmpty.wait(lock, [this] {return !m_deqDatas.empty(); });
T front(m_deqDatas.front());
m_deqDatas.pop_front();
return front;
}
bool Empty()
{
//这个lock用于对消费者,生产者线程的互斥
std::lock_guard<std::mutex> lock(m_mutex);
return m_deqDatas.empty();
}
size_t Size()
{
//这个lock用于对消费者,生产者线程的互斥
std::lock_guard<std::mutex> lock(m_mutex);
return m_deqDatas.size();
}
private:
std::deque<T> m_deqDatas;
//互斥量,对队列进行同步保护
std::mutex m_mutex;
//用于限制消费者线程
std::condition_variable m_notEmpty;
};
main函数
void consumer(UnBoundedQueue<int>* dep)
{
auto thread_id = std::this_thread::get_id();
std::this_thread::sleep_for(std::chrono::seconds(3));
while (1)
{
std::cout << "thread id " << thread_id << "," << dep->Get() << std::endl;
}
}
int main()
{
UnBoundedQueue<int> queue;
auto f = std::async(std::launch::async, consumer, &queue);
for (int i = 0; i < 100; ++i)
{
queue.Put(i);
}
f.wait();
}
-
无界队列,生产者线程并不需要等待队列中有空位,所以相比有界队列不需要等待空位的条件变量。
-
信号量m_notEmpty是用于在容器的容量上限制消费者线程,当容量为空时,消费者线程应该被阻塞。