C++ 多线程同步实例:使用 Condition Variable 实现生产者-消费者模型(看完不懂去小红薯网暴我_doge)

目录

写在前面

正文开始(不想看文字的可以直接跳到代码)

1.条件变量是什么?干嘛的?

2.为什么要用条件变量?

3.如何使用?

1.wait函数(理解了这个函数,条件变量就懂了)

2.notify_one

4. 完整使用示例

1. 经典生产者-消费者模型

2.实战-简化项目代码(网络收数据)

最后


写在前面

线程同步方式很多种,但本人觉得工作中用得较多的就是条件变量,至于条件变量概念什么的,本文只会简单提及,因为官网的才是权威请移步cpp官方文档。我也不会贴什么官方文档的东西糊弄,本人也反感。

正文开始(不想看文字的可以直接跳到代码)


我们学一个知识点时,不该只是会用,还要知道其原理(当然有些原理太难,就算了,比如条件变量),但也要知道为什么要这样用,对知识点发出疑问,才是真正理解(正所谓知其然而知其所以然)。 


1.条件变量是什么?干嘛的?


概念:条件变量是一种用于线程间通信的同步机制,主要用于线程间的“等待-通知”模型。它配合互斥锁(std::mutex)使用,使一个线程可以等待某个条件成立,而另一个线程在条件满足后通知其继续执行

简单的说,就是B线程一直在等着,没有继续往下执行,然后A线程通过条件变量让B不用等了,可以往下执行了。

2.为什么要用条件变量?

通过概念,我们知道条件变量是等待-通知,那我们用其他线程同步方式可以不呢,可以的,兄弟,比如互斥锁(std::mutex), 它也可以让某个线程等在那里,直到条件成立,伪代码

while(true) {
    if (条件成立)
        break;
}

那这里是不是一直在占着CPU资源,对吧。那condition_variable就避免了这种让CPU空转的问题。conditon_variable是怎么做的呢,它通过操作系统机制挂起线程,等条件满足时唤醒,线程就不会一直在那儿空转了,当然背后的机制就不是我们探讨的啦。

3.如何使用?

c++是面向对象的语言,那这个condition_varible,肯定是一个类(好像是废话一样)。那我们只要了解这个类有哪些函数,函数作用是什么就可以了。贴个简化的源码,我只写了几个常用的函数,主要我觉得把这几个都搞懂了,其他的就也懂了。

class condition_variable { // class for waiting for conditions
public:
    using native_handle_type = _Cnd_t;

    condition_variable() {}

    ~condition_variable() noexcept {}

    condition_variable(const condition_variable&) = delete;
    condition_variable& operator=(const condition_variable&) = delete;

    void notify_one() noexcept { // wake up one waiter
    }

    void notify_all() noexcept { // wake up all waiters
    }

    void wait(unique_lock<mutex>& _Lck) { // wait for signalnothrow
    }

    template <class _Predicate>
    void wait(unique_lock<mutex>& _Lck, _Predicate _Pred) { // wait for signal and test predicate
        while (!_Pred()) {
            wait(_Lck);
        }
    }
};

还是贴下官方文档的


正常我们学习,写一些玩具代码,只会用到 notify_one、wait这俩函数,真的相信我,不过这俩懂了,条件变量也就懂了。

1.wait函数(理解了这个函数,条件变量就懂了)
void wait(unique_lock<mutex>& _Lck)
template <class _Predicate>
void wait(unique_lock<mutex>& _Lck, _Predicate _Pred)

作用:阻止当前线程直到条件变量唤醒(官翻)。

当我们调用了这个函数,线程就会阻塞,不往下执行,参数中的Mutex,在调用后,条件变量内部会自动将其释放,解锁。

这个函数wait(unique_lock<mutex>& _Lck),参数是 mutex。前面概念说到条件变量只是用于线程间等待-通知,但不负责保护共享资源,划重点哦。所以我们实际在写代码的时候,会配合上mutex使用。

void wait(unique_lock<mutex>& _Lck, _Predicate _Pred)这个版本就跟下面代码中while一样,后续解释。

贴一个消费者线程通用写法的伪代码

1 std::condition_variable cv;
2 std::mutex mx;
3 //int buffer,用于模拟缓冲区数据,实际应用可能是字符数组或者其他类型,但原理相同
4 int buffer = 0;

5 //假装这是在一个线程里面
6 {
7 std::unique_lock<std::mutex> lock(mx);

8 ///两种写法效果相同, 可以看wait的源码,上面的写法,等价于上面这种,只是觉得上面这个写法更适合我们理解
9 #if 1
10 while( buffer == 0 ) {
11     cv.wait(lock);
12 }
13 #else
14 cv.wait(lock, [&](){ return buffer != 0; });
15 #endif

16//可以对buffer,做某种操作了
}

通过代码解释哈,线程同步的本质就是对共享资源的访问。如上代码,我们先用mutex上锁,因为buffer相当于共享资源,下面的操作,肯定是对buffer操作。

当我们执行到while这里的时候,先判断buffer == 0,等于0,就代表没有数据给我们用,那就执行wait函数,我们调用了wait函数,这个时候此线程就会阻塞到这里,不会接着往下执行了,并且mutex,会释放,让其他线程可以拿到锁,然后对buffer进行操作,那其他线程肯定会将buffer赋值嘛,表示有数据了,然后其他线程就会调用 cv.notify_one 让现在这个线程唤醒,就是接着往下执行,那么下一步就走到了 while的判断这里,此时buffer != 0, 就跳出了循环,往下执行,对buffer进行操作。

这里之所以要把mutex作为参数传给wait函数,就是我们调用了wait后,条件变量内部就会把mutex释放了,好让其他线程拿到锁操作buffer。

大概逻辑就是

线程 调用wait----> 阻塞(其他线程修改buffer, 唤醒此线程)-->接着往下执行,跳出循环--->往下执行

上面代码执行逻辑: 10行(buffer为0) -->11行(线程wait) -->阻塞(等待其他线程唤醒)-->10行(buffer不为0) -->16行。

while( buffer != 0 ) {
    cv.wait(lock);
}

可能有人对这里有疑惑,为啥要while,只写wait行不行,不也是会阻塞吗。不行!!!

原因:

1. 条件变量可能会被操作系统虚假唤醒,那这时的buffer可能还是0,那不写while,就会访问到错误的buffer。

2.如果这个线程一开始执行,你的buffer就不是0,那我们就不用wait了。

2.notify_one
    void notify_one() noexcept { // wake up one waiter
    }

作用:唤醒调用了wait的线程,让其继续往下执行。

这个函数使用起来很简单,就是该线程(生产者线程)修改了共享资源,然后调用notify_one唤醒调用了wait的线程(比如上面说的消费者线程)。

贴一个生产者线程通用伪代码

std::condition_variable cv;
std::mutex mx;
//int buffer,用于模拟缓冲区数据,实际应用可能是字符数组或者其他类型,但原理相同
int buffer = 0;

//假装在一个生产者线程
{
std::unique_lock<std::mutex> lock(mx);
buffer = i;
std::cout << "Producer buffer = " << i << "\t";
cv.notify_one();
}

生产者线程比较简单, 就是拿到mutex的锁后,修改共享资源后,调用notify_one()唤醒在等待的线程继续往下执行。

4. 完整使用示例

1. 经典生产者-消费者模型

可能有以下两种情况:

1. 线程1先执行:它拿锁,设置 i = 1,然后 notify_one(),线程2还没到 wait(),这时线程2后续执行时发现 i == 1,while( i != 1) { cv.wait(lock); } 立即通过,不会阻塞。
2. 线程2先执行:它拿锁,调用 wait(),这时会释放锁并阻塞等待;线程1后执行,拿锁,修改 i = 1,调用 notify_one(),唤醒线程2,线程2拿回锁,检查 i == 1 条件成立,继续执行。

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>

std::mutex mx;
std::condition_variable cv;
int i = 0;

int main() {
    // 启动线程1:设置 i=1 并通知
    std::thread t1([&](){
        std::unique_lock<std::mutex> lock(mx);
        i = 1;
        cv.notify_one();  // 通知等待线程
    });

    // 启动线程2:等待 i==1 后打印
    std::thread t2([&](){
        std::unique_lock<std::mutex> lock(mx);
        while( i != 1)
        {
            cv.wait(lock);
        }
        std::cout << "Thread ID: " << std::this_thread::get_id() << " i=" << i << std::endl;
    });

    // 等待线程结束
    t1.join();
    t2.join();

    return 0;
}
2.实战-简化项目代码(网络收数据)

实际项目的情况,比如我们收到网络数据,一个线程收数据,另一个线程解析数据,这也是生产者消费者模型的场景。 收数据的线程,有数据后,写入缓冲区,然后通知解析线程运行。

因为我是垃圾Qt仔,就使用的Qt的网络库写的示例代码。

#include <QApplication>
#include <QThreadPool>

#include <condition_variable>
#include <mutex>

#include <QTcpServer>
#include <QTcpSocket>

//模拟实际解析数据帧
int FrameLength = 0;
void process(QByteArray *data)
{
    while (true) {
        if (data->size() < FrameLength)
            break;

        int headerIndex = data->indexOf(char(0xF5));
        if (headerIndex == -1)
            break;

        // 数据不足以构成一帧
        if (data->size() - headerIndex < FrameLength)
            break;

        QByteArray frame = data->mid(headerIndex, FrameLength);
        if (frame.endsWith(char(0x5F))) {
            data->remove(headerIndex, FrameLength);
            qDebug() << "解析到帧:" << frame.toHex(' ').toUpper();
        } else {
            // 避免跳过合法帧头
            data->remove(headerIndex, 1);
        }
    }
}


int main(int argc, char *argv[])
{
    //模拟线程什么时候该退出,比如实际应用时,软件退出
    std::atomic<bool> bExit = false;
    std::atomic<int> sendDataSize = 0, recvDataSize = 0;


    //模拟发数据的一端
    QThreadPool::globalInstance()->start([&](){
        QTcpServer tcpServer;
        qDebug()<< "---------->TcpServer.listen:" << tcpServer.listen(QHostAddress("127.0.0.1"), 8001);

        if (tcpServer.waitForNewConnection(3000))
        {
            QTcpSocket *socket = tcpServer.nextPendingConnection();
            QByteArray data = QByteArray::fromHex("F5123456785F");
            FrameLength =  data.size();
            sendDataSize = data.size() * 50;
            for(int i = 0; i < 50; ++i)
            {
                socket->write(data);
                socket->waitForBytesWritten();
                // QThread::msleep(500);
            }

            QByteArray data2 = QByteArray::fromHex("F5785634125F");
            sendDataSize += data2.size();
            socket->write(data2);
            socket->waitForBytesWritten();
        }
        qDebug()<< "------------->Send data is end.";
    });

    QByteArray m_buffer, m_bufferBak;
    std::condition_variable cv;
    std::mutex mx;

    //解析线程
    QThreadPool::globalInstance()->start([&](){
        while(true) {
            std::unique_lock<std::mutex> lock(mx);

            //判断此时是否有数据来,没有的话,就等
            cv.wait(lock, [&](){
                return !m_buffer.isEmpty();
            });

            if (!m_buffer.isEmpty()) {
                //迅速把缓冲区的数据取出来,然后解锁,让收数据的线程继续工作
                //不然因为解析的时间过长,影响收数据线程工作。
                QByteArray recvData = m_buffer;
                m_buffer.clear();
                lock.unlock();

                recvDataSize += recvData.size();

                //解析数据逻辑
                m_bufferBak.append(recvData);
                process(&m_bufferBak);
            }

            if (recvDataSize == sendDataSize) {
                bExit = true;
                break;
            }

        }
    });

    QTcpSocket client;
    client.connectToHost(QHostAddress("127.0.0.1"), 8001);
    qDebug() << "client.waitForConnected()=" << client.waitForConnected();

    //模拟收数据线程,为了方便就在主线程写。
    while (true){
        if (bExit)
            break;

        if (client.waitForReadyRead()) {
            QByteArray data = client.readAll();

            //有数据,写到缓冲区后,马上通知解析线程处理
            std::unique_lock<std::mutex> lock(mx);
            m_buffer.append(data);
            cv.notify_one();
        }
    }

    QThreadPool::globalInstance()->waitForDone();

    return 0;
}

最后

初次发稿,写得不好的地方,请指正,或有不明白的地方,在评论区指出后,在修改内容。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值