多线程虚假唤醒

引言

单生产者多消费者——非线程安全

在前面多线程竞态-单资源竞态的生产者和消费者例子中,消费者是无论是否有产品都能消费,甚至产品数量为负数也能消费,这是不符合生活常识的。那么,此处我们在消费者侧增加数量的判定,并且改为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的值
11
21
30
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消费者线程2productCount
1解锁解锁0
2阻塞释放CPU阻塞释放CPU0
3生产+11
4通知消费者 notify_all1
5被唤醒,获得CPU执行权被唤醒,获得CPU执行权1
6加锁成功加锁失败,等待1
7消费 -10
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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值