引言
单生产者多消费者——非线程安全
在前面多线程竞态-单资源竞态的生产者和消费者例子中,消费者是无论是否有产品都能消费,甚至产品数量为负数也能消费,这是不符合生活常识的。那么,此处我们在消费者侧增加数量的判定,并且改为1个生产场景,多个(假定2个)消费者,更加贴切生活:
#include <cstdint>
#include <functional>
#include <iostream>
#include <thread>
#include <atomic>
#include <vector>
#include <string>
#include <mutex>
using namespace std;
// 共享资源:产品个数
atomic_int32_t productCount{0};
mutex recordMtx;
vector<string> runInfos;
void Record(const string& name)
{
lock_guard<mutex> guard(recordMtx);
runInfos.push_back(name + " productCount:" + to_string(productCount));
}
// 生产者:生产
void Produce()
{
productCount += 1;
Record("producer");
}
// 消费者:消费
void Consume()
{
productCount -= 1;
Record("consumer");
}
// 创建两个线程,分别充当生产者和消费者
void ChangeProductCount(uint32_t runCount)
{
auto run = [=](function<void()> action, uint32_t productCountPerRunner) {
for (uint32_t i = 0; i < productCountPerRunner; ++i) {
this_thread::sleep_for(chrono::milliseconds(1));
action();
}
};
thread producer(run, Produce, runCount);
uint32_t consumerCount = 2;
vector<thread> consumers;
for (uint32_t i = 0; i < consumerCount; ++i) {
consumers.emplace_back(thread(run, Consume, runCount/consumerCount));
}
producer.join();
for (auto& th : consumers){
th.join();
}
}
int main()
{
ChangeProductCount(6);
for (auto& info : runInfos) {
cout << info << endl;
}
return 0;
}
程序的运行结果如下所示:
producer productCount:-1
consumer productCount:-1
consumer productCount:-1
producer productCount:0
consumer productCount:-1
consumer productCount:-2
producer productCount:-1
consumer productCount:-2
consumer productCount:-3
producer productCount:-2
producer productCount:-1
producer productCount:0
上述结果,不是我们所期望结果,如果我们在消费时,判定是否有产品,即 productCount > 0
,且同时也要保证产品都消费完,我们可以改造消费函数:
// 消费者:消费
void Consume()
{
if (productCount > 0) { // ①
return;
}
// 等待产品数量>0,才进行消费
productCount -= 1; // ②
Record("consumer"); // ③
}
但是,运行结果,还是不是期望的效果:
producer productCount:-1
consumer productCount:-1
consumer productCount:-1
producer productCount:0
consumer productCount:-1
consumer productCount:-1
producer productCount:-1
producer productCount:0
producer productCount:1
producer productCount:2
单生产者多消费者-线程安全
我们来分析一下上述的生产者-消费者代码,在消费之前已经等待产品数量>0了,为什么还是有问题呢?我们借鉴多线程竞态-单资源竞态原因的分析方法,同时我们此处使用的已经是原子类型的变量了,下面的分析表格,仅分析了消费者的一种场景:
时间序列 | 线程1执行语句 | 线程2执行语句 | productCount的值 |
---|---|---|---|
1 | 1 | ||
2 | ① | ① | 1 |
3 | ② | 0 | |
4 | ② | -1 | |
5 | ③ | -1 |
从上述表格来看,就是未对上述的3个步骤做原子化处理,同样的,生产者的处理也是类似。于是,对已有的代码进行修改:
#include <cstdint>
#include <functional>
#include <iostream>
#include <thread>
#include <vector>
#include <string>
#include <mutex>
using namespace std;
// 共享资源:产品个数
mutex productMutex;
int32_t productCount = 0;
void Show(const string& name)
{
string info = name + " productCount:" + to_string(productCount);
std::cout << info << std::endl;
}
// 生产者:生产
void Produce()
{
lock_guard<mutex> guard(productMutex);
productCount += 1;
Show("producer");
}
// 消费者:消费
void Consume()
{
lock_guard<mutex> guard(productMutex);
if (productCount <= 0) {
return;
}
// 等待产品数量>0,才进行消费
productCount -= 1;
Show("consumer");
}
// 创建两个线程,分别充当生产者和消费者
void ChangeProductCount(uint32_t runCount)
{
auto run = [=](function<void()> action, uint32_t productCountPerRunner) {
for (uint32_t i = 0; i < productCountPerRunner; ++i) {
this_thread::sleep_for(chrono::milliseconds(1));
action();
}
};
thread producer(run, Produce, runCount);
uint32_t consumerCount = 2;
vector<thread> consumers;
for (uint32_t i = 0; i < consumerCount; ++i) {
consumers.emplace_back(thread(run, Consume, runCount/consumerCount));
}
producer.join();
for (auto& th : consumers){
th.join();
}
}
int main()
{
ChangeProductCount(6);
return 0;
}
运行输出结果:
producer productCount:1
consumer productCount:0
producer productCount:1
consumer productCount:0
producer productCount:1
producer productCount:2
producer productCount:3
producer productCount:4
虽然说,上述代码已经达到线程安全了,消费者线程不能达到预期次数的消费,即出现了消费者线程无效的消费。
无效消费,产生的原因是,消费者只是判定了产品数量是否满足消费条件,但未继续等待,因此就浪费了一次消费机会。那我们再把判定条件改为while
循环等待方式:
// 消费者:消费
void Consume()
{
lock_guard<mutex> guard(productMutex);
while (productCount <= 0) {
;
}
// 等待产品数量>0,才进行消费
productCount -= 1;
Show("consumer");
}
输出结果:
producer productCount:1
consumer productCount:0
// 程序卡住,再无输出
再次分析发现,一旦消费者发现产品数量为0,一直在等待生产者的生产,但是消费者占用了锁,生产者也获取不到锁,这就造成了一个隐形的死锁状态。如果要破除这种状态,在前面的多线程死锁-避免死锁方法中,也介绍了打破占有且等待的条件,即可解除死锁状态。对消费者线程的函数再次修改如下:
// 消费者:消费
void Consume()
{
while (true) {
productMutex.lock();
if (productCount > 0) {
break;
}
productMutex.unlock(); // 条件不满足时,先释放锁,下次判定再加锁
}
// 等待产品数量>0,才进行消费
productCount -= 1;
Show("consumer");
productMutex.unlock();
}
程序输出结果:
producer productCount:1
consumer productCount:0
producer productCount:1
consumer productCount:0
producer productCount:1
consumer productCount:0
producer productCount:1
consumer productCount:0
producer productCount:1
consumer productCount:0
producer productCount:1
consumer productCount:0
条件变量-虚假唤醒
前面小节介绍的方式,能成功的运行且能达到期望效果,为什么还要使用条件变量呢?可能很多人都有这个疑问,其实我们再深入分析就会发现,如果生产者线程半个小时才生产一个产品,那么消费者线程在这半个小时内也没闲着,一直在判定产品数量的个数是否大于0了。对于消费者线程来说,这是一个低效的工作方式,CPU的资源白白浪费了。因此,条件变量可以让这个过程变得更加高效,生产者生产了产品,通知消费者,已经生产了产品了,消费者可以使用了。举例一个消费者线程如下:
#include <cstdint>
#include <functional>
#include <iostream>
#include <thread>
#include <vector>
#include <string>
#include <mutex>
#include <condition_variable>
using namespace std;
// 共享资源:产品个数
condition_variable cv;
mutex productMutex;
int32_t productCount = 0;
void Show(const string& name)
{
string info = name + " productCount:" + to_string(productCount);
std::cout << info << std::endl;
}
// 生产者:生产
void Produce()
{
{ // 对保护的资源修改后,应该释放锁
lock_guard<mutex> guard(productMutex);
productCount += 1;
Show("producer");
}
// 调用通知接口时,需保证接收通知的线程能第一时间获取到锁
cv.notify_all();
this_thread::sleep_for(chrono::milliseconds(1)); // 通知后,等待一会儿,避免下一次执行时间间隔太短,导致通知丢失。
}
// 消费者:消费
void Consume()
{
unique_lock<mutex> unique(productMutex); // 加锁
cv.wait(unique); // 解锁,阻塞睡眠释放CPU资源;唤醒获得CPU资源,加锁
productCount -= 1;
Show("consumer");
}
// 创建两个线程,分别充当生产者和消费者
void ChangeProductCount(uint32_t runCount)
{
auto run = [=](function<void()> action, uint32_t productCountPerRunner) {
for (uint32_t i = 0; i < productCountPerRunner; ++i) {
action();
}
};
uint32_t consumerCount = 1;
vector<thread> consumers;
for (uint32_t i = 0; i < consumerCount; ++i) {
consumers.emplace_back(thread(run, Consume, runCount/consumerCount));
}
thread producer(run, Produce, runCount);
producer.join();
for (auto& th : consumers){
th.join();
}
}
int main()
{
ChangeProductCount(6);
return 0;
}
运行结果如下:
producer productCount:1
consumer productCount:0
producer productCount:1
consumer productCount:0
producer productCount:1
consumer productCount:0
producer productCount:1
consumer productCount:0
producer productCount:1
consumer productCount:0
producer productCount:1
consumer productCount:0
上述单个消费者线程的场景,运行结果符合预期效果。但是,如果将消费者线程数量该为2,即:uint32_t consumerCount = 2
,运行结果又当如何:
producer productCount:1
consumer productCount:0
consumer productCount:-1
producer productCount:0
consumer productCount:-1
consumer productCount:-2
producer productCount:-1
consumer productCount:-2
consumer productCount:-3
producer productCount:-2
producer productCount:-1
producer productCount:0
问题分析:
a. 单消费者线程是运行成功的,仅仅将原来的单消费者线程改成了2个消费者线程,那问题必然是在消费者的处理逻辑中。
b. 两个消费者线程之间的运行情况,着重分析如下:
时间序列 | 生产者线程 | 消费者线程1 | 消费者线程2 | productCount |
---|---|---|---|---|
1 | 解锁 | 解锁 | 0 | |
2 | 阻塞释放CPU | 阻塞释放CPU | 0 | |
3 | 生产+1 | 1 | ||
4 | 通知消费者 notify_all | 1 | ||
5 | 被唤醒,获得CPU执行权 | 被唤醒,获得CPU执行权 | 1 | |
6 | 加锁成功 | 加锁失败,等待 | 1 | |
7 | 消费 -1 | 0 | ||
8 | 解锁 | 加锁成功 | 0 | |
9 | 消费 -1 | -1 |
上面的情况就是虚假唤醒,造成了线程的误唤醒,唤醒之后又不满足处理条件,这就造成了错误的处理了产品消费。
条件变量-唤醒后判定条件
在被唤醒后,我们再判定一下是否满足处理条件,如果满足,再进行后续的处理就可以了。有两种方案,这两种方案都是等效的,并且在条件变量中再添加条件判定,还能解决唤醒信号丢失的情况。
方案一:实现while循环判定
// 消费者:消费
void Consume()
{
unique_lock<mutex> unique(productMutex); // 加锁
while (productCount <= 0) {
cv.wait(unique); // 等待通知,解锁,释放CPU资源;收到通知,再加锁
}
productCount -= 1;
Show("consumer");
}
方案二:利用库函数的while循环判定
// 消费者:消费
void Consume()
{
unique_lock<mutex> unique(productMutex); // 加锁
cv.wait(unique, [&](){ return productCount > 0; }); // 等待通知,解锁,释放CPU资源;收到通知,再加锁
productCount -= 1;
Show("consumer");
}
运行结果如下:
producer productCount:1
consumer productCount:0
producer productCount:1
consumer productCount:0
producer productCount:1
consumer productCount:0
producer productCount:1
consumer productCount:0
producer productCount:1
consumer productCount:0
producer productCount:1
consumer productCount:0