生产者与消费者C++
生产者与消费者模型
假设有两个线程A、B和一个固定大小的缓冲区,A线程生产数据放入缓冲区,B线程从缓冲区中取出数据进行操作,这就是一个简单的生产者-消费者模型。这里线程A相当于生产者,线程B相当于消费者。
主要实现的功能是:
- 生产者生产内容
- 消费者消费内容
- 固定大小的缓冲区,超过缓冲区大小,生产者停止生产,反之一直等待
- 缓冲区有内容时消费者进行消费,缓冲区为空时等待
- 加锁,保证线程安全
生产者与消费者模型的优点
-
解耦合:将生产者类和消费者类进行解耦,消除代码之间的依赖性,简化工作负载的管理。
-
复用:对生产者类和消费者类进行独立的复用与扩展。
-
调整并发数:由于生产者和消费者的处理速度是不一样的,可以调整并发数,来提高任务的处理速度。
-
异步:生产者只需要关心缓冲区是否还有数据,不需要等待消费者处理完;同样的对于消费者来说,也只需要关注缓冲区的内容,不需要关注生产者,将一个耗时的流程拆成生产和消费两个阶段,通过异步的方式支持高并发。
-
支持分布式:生产者和消费者通过队列进行通讯,不需要运行在同一台机器上。
代码实现生产者与消费者模型
#include <iostream>
#include <thread>
#include <mutex>
#include <string>
#include <condition_variable>
#include <queue>
using namespace std;
queue<int> m_queue;
condition_variable m_cv; //条件变量
mutex tex;
void producer()
{
for (int i = 0; i < 10; i++)
{
{
unique_lock<mutex> lock(tex);
m_queue.push(i); //生产者将数据放入队列中
m_cv.notify_one(); //告诉消费者取数据
cout << "producer: " << i << endl;
}
this_thread::sleep_for(chrono::microseconds(100));
}
}
void customer()
{
while (1)
{
unique_lock<mutex> lock(tex);
m_cv.wait(lock, []() { //wait的第二个参数为true时解除阻塞,为false时阻塞当前线程
return !m_queue.empty(); //当队列不为空(即有数据)时,lambda表达式返回true
});
int value = m_queue.front();
m_queue.pop();
cout << "customer: " << value << endl;
}
}
int main()
{
thread t1(producer); //创建生产者线程
thread t2(customer); //创建消费者线程
t1.join();
t2.join();
return 0;
}
条件变量condition_variable
当 std::condition_variable 对象的某个 wait 函数被调用的时候,它使用 std::unique_lock(通过 std::mutex) 来锁住当前线程。当前线程会一直被阻塞,直到另外一个线程在相同的 std::condition_variable 对象上调用了 notification 函数来唤醒当前线程。
void wait (unique_lock<mutex>& lck);
void wait (unique_lock<mutex>& lck, Predicate pred);
std::condition_variable 提供了两种 wait() 函数。当前线程调用 wait() 后将被阻塞,直到另一个线程调用 notify_one或notify_all 唤醒了当前线程,该函数会自动调用 lck.unlock() 释放锁,使得其他被阻塞在锁竞争上的线程得以继续执行。
在第二种情况下,只有当 pred 条件为 false 时调用 wait() 才会阻塞当前线程,并且在收到其他线程的通知后只有当 pred 为 true 时才会被解除阻塞。
在上述代码中,使用的时第二种wait方法。消费者线程调用了condition_variable的wait方法,线程被阻塞,在生产者线程中调用了condition_variable的notify_one方法,唤醒消费者线程。
unique_lock
unique_lock和lock_guard基本用法相同,构造时默认加锁,析构时默认解锁,但unique_lock有个好处就是可以手动解锁。unique_lock比lock_guard灵活很多,效率上差一点,内存占用多一点。
unique_lock的第二个参数
unique_lock(_Mutex& _Mtx, adopt_lock_t)
- std::adopt_lock:表示
_Mtx
这个互斥量已经被lock了(必须要把互斥量提前lock了 ,否者会报异常);
unique_lock(_Mutex& _Mtx, defer_lock_t)
- 使用给定的互斥量
_Mtx
进行初始化,但不对该互斥量进行加锁操作。
unique_lock(_Mutex& _Mtx, try_to_lock_t)
- 使用给定的互斥量
_Mtx
进行初始化,并尝试对该互斥量进行加锁操作。如果加锁失败,则创建的std::unique_lock
对象不与任何互斥量关联。
unique_lock成员函数
- lock()
- unlock()
- try_lock():尝试给互斥量加锁,如果拿不到锁,返回false,如果拿到了锁,返回true,这个函数是不阻塞的;
- release():返回它所管理的mutex对象指针,并释放所有权;也就是说,这个unique_lock和mutex不再有关系。
- try_lock_for:尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁,或者超过了指定的时间。
- try_lock_until:尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁,或者超过了指定的时间点。
为什么有时候需要unlock();锁住的代码段越少,执行越快,整个程序运行效率越高。一般我们将锁住的代码多少成为锁的粒度,粒度一般用粗细来描述;
-
锁住的代码少,这个粒度叫细,执行效率高;
-
锁住的代码多,这个粒度叫粗,执行效率低;
要学会尽量选择合适粒度的代码进行保护,粒度太细,可能漏掉共享数据的保护,粒度太粗,影响效率。