生产者和消费者模型

一.简单的生产者消费者模式

  1. 这是用C++ 写的一个生产者消费者模型,这个模型常常用在图像推理过程中。
    仅仅是用作记录
    以上的内容基本参考TensorRT中的线程知识
#include <thread>
#include <queue>
#include <mutex>
#include <string>
#include <stdio.h>
#include <chrono>

using namespace std;

queue<string> qjobs_;

void video_capture(){

    int img_id = 0;
    while(true){
        
        char name[100];
        sprintf(name, "PIC-%d", img_id++);
        printf("生产了一个新图片: %s\n", name);
        qjobs_.push(name);
        this_thread::sleep_for(chrono::milliseconds(1000));
    }
}

void infer_worker(){
    
    while(true){
        
        if(!qjobs_.empty()){
            auto img = qjobs_.front();
            qjobs_.pop();
            
            printf("消费掉一个图片: %s\n", img.c_str());
            this_thread::sleep_for(chrono::milliseconds(1000));
        }
        this_thread::yield();
    }
}

int main(){
    
    thread t0(video_capture);
    thread t1(infer_worker);

    t0.join();
    t1.join();
    return 0;
}

补充:对于this_thread::yield()的理解
在多任务操作系统中,"时间片"通常指的是每个线程或进程被分配的连续执行时间。操作系统通过调度算法来决定何时切换到另一个线程或进程,以确保所有活动线程都有机会执行。时间片的长度是由操作系统的调度策略和配置参数决定的。
在C++中,std::this_thread::yield()并不直接控制时间片的长度。它的目的是通知操作系统当前线程愿意放弃其时间片,以便其他线程有机会执行。具体的时间片划分和线程调度由操作系统内核负责。
因此,std::this_thread::yield() 并不是精确定义的时间控制工具,而是一种线程协作的手段,用于提醒操作系统当前线程愿意主动放弃执行。如上例中所示,它的使用可以使得多个线程更公平地共享 CPU 时间,但并不控制具体的时间片长短。这是为了让操作系统更好的执行调度

二.带锁的生产者消费模型

当领用高频次时,就会产生崩溃,这是一个共享资源访问问题。所以这个是一定要加上锁的

#include <thread>
#include <queue>
#include <mutex>
#include <string>
#include <stdio.h>
#include <chrono>

using namespace std;

queue<string> qjobs_;
mutex lock_;

void video_capture(){

    int img_id = 0;
    while(true){
        {
            lock_guard<mutex> l(lock_);
            char name[100];
            sprintf(name, "PIC-%d", img_id++);
            printf("生产了一个新图片: %s\n", name);
            qjobs_.push(name);
        }
        this_thread::sleep_for(chrono::milliseconds(1000));
    }
}

void infer_worker(){
    
    while(true){
        
        if(!qjobs_.empty()){
            {
                lock_guard<mutex> l(lock_);
                auto img = qjobs_.front();
                qjobs_.pop();
                
                printf("消费掉一个图片: %s\n", img.c_str());
            }
            this_thread::sleep_for(chrono::milliseconds(1000));
        }
        this_thread::yield();
    }
}

int main(){
    
    thread t0(video_capture);
    thread t1(infer_worker);

    t0.join();
    t1.join();

    return 0;
}

std::mutex是一个互斥锁。当一个线程获得这个锁时,其他的所有线程都必须等待直到锁被释放。lock_guard是一个辅助对象,它在构造时自动锁定给定的互斥量,并在析构时自动解锁。当控制流离开locl_guard的作用域时,锁会自动释放,无论是正常离开还是因为异常。

三.时间不一致的处理

如果将生产时间修改为0.5s,消费时间不变。这样产生的问题就是:队列溢出问题,即生产太快,消费太慢。就是生产频率高于消费频率,则队列出现堆积现象。设置一个上限,当队列中的数据达到一定数据,就不会继续再继续生产了。
if(qjobs_.size()>limit)
	wait();

可以使用 condition_variable 很优雅的做到这个事情

#include <thread>
#include <queue>
#include <mutex>
#include <string>
#include <stdio.h>
#include <chrono>
#include <condition_variable>

using namespace std;

queue<string> qjobs_;
mutex lock_;
condition_variable cv_;
const int limit_ = 5;

void video_capture(){

    int img_id = 0;
    while(true){
        {
            unique_lock<mutex> l(lock_);
            char name[100];
            sprintf(name, "PIC-%d", img_id++);
            printf("生产了一个新图片: %s, qjobs_.size = %d\n", name, qjobs_.size());

            cv_.wait(l, [](){
                // return false,表示继续等待
                // return true,表示不等待,跳出wait
                return qjobs_.size() < limit_;
            });  
            // 如果队列满了,我不生产,我去等待队列有空间再生产
            // 通知的问题,如何通知到 wait,让它即时的可以退出
            qjobs_.push(name);
        }
        this_thread::sleep_for(chrono::milliseconds(500));
    }
}

void infer_worker(){  
    while(true){     
        if(!qjobs_.empty()){
            {
                lock_guard<mutex> l(lock_);
                auto img = qjobs_.front();
                qjobs_.pop();
                
                // 消费掉一个,就可以通知 wait,去跳出等待
                cv_.notify_one();
                printf("消费掉一个图片: %s\n", img.c_str());
            }
            this_thread::sleep_for(chrono::milliseconds(1000));
        }
        this_thread::yield();
    }
}
int main(){
    thread t0(video_capture);
    thread t1(infer_worker);
    t0.join();
    t1.join();

    return 0;
}

在生产者线程中,使用了cv_.wait()方法。这个方法会阻塞当前线程直到满足给定的条件。
cv_.wait()
I 是一个unique_lock,它用于保护条件变量和相关资源(队列qjobs_)
Lambda 函数作为条件,当此函数返回 true 时,wait() 会返回并允许线程继续执行。如果条件不满足(返回 false),wait() 会自动释放锁并阻塞当前线程,直到条件变量被通知。
最后在消费者线程中,每次从队列中取出一张图片,都会调用cv_.notify_one()。这会唤醒一个等待在条件变量上的线程。如果生产者线程因为队列已满而被阻塞的话,它会唤醒生产者线程。

四.生产者如何拿到消费者的反馈

消费者在拿到图片对图片做了处理,比如进行了模型推理,那拿到了推理结果该如何送到生产者线程中?正常来说 video_capture 把图片给消费者线程进行推理,我们希望拿到推理的结果,然后跟推理之前的图像一起进行画框,然后走下面的流程。说我们有多个模型,infer_worker 有多个,比如目标检测、目标分割、人脸检测、人脸识别等等,所以你会发现你有多个消费者,意味着你有多个队列。

// 同步模式
// detection -> infer
// face -> infer
// feature -> infer
// 异步模式
// detection -> push
// face -> push
// feature -> push

同步模式下你要一个个模型推理,而异步模式下你只需要将图片一个个 push 就行,异步模式的优点在于你可以一次进行 3 个结果的回收,然后进行处理。
如何把结果从消费者线程返回到生产者线程,这时候引入一个概念,即future,promise

#include <thread>
#include <queue>
#include <mutex>
#include <string>
#include <stdio.h>
#include <chrono>
#include <condition_variable>
#include <memory>
#include <future>

using namespace std;

struct Job{
    shared_ptr<promise<string>> pro;
    string input;
};

queue<Job> qjobs_;
mutex lock_;
condition_variable cv_;
const int limit_ = 5;

void video_capture(){

    int img_id = 0;
    while(true){
        Job job;
        {
            unique_lock<mutex> l(lock_);
            char name[100];
            sprintf(name, "PIC-%d", img_id++);
            printf("生产了一个新图片: %s\n", name);

            cv_.wait(l, [](){
                // return false,表示继续等待
                // return true,表示不等待,跳出wait
                return qjobs_.size() < limit_;
            });
            
            // 如果队列满了,我不生产,我去等待队列有空间再生产
            // 通知的问题,如何通知到 wait,让它即时的可以退出
            job.pro.reset(new promise<string>());
            job.input = name;
            qjobs_.push(job);

            // 等待这个 job 处理完毕,拿结果
            // job.pro->get_future() 返回的其实是 future 对象
            // .get 过后,实现等待,直到 promise->set_value 被执行了,这里的返回值就是 result
            // 拿到推理结果,跟推理之前的图像一起进行画框,然后走下面的流程
        }
        auto result = job.pro->get_future().get();
        printf("Job %s -> %s\n", job.input.c_str(), result.c_str());

        this_thread::sleep_for(chrono::milliseconds(500));
    }
}

void infer_worker(){
    
    while(true){
        
        if(!qjobs_.empty()){
            {
                lock_guard<mutex> l(lock_);
                auto pjob = qjobs_.front();
                qjobs_.pop();     
                // 消费掉一个,就可以通知 wait,去跳出等待
                cv_.notify_one();
                printf("消费掉一个图片: %s\n", pjob.input.c_str());
                auto result = pjob.input + " --- infer";
                // new_pic 送回到生产者,怎么办
                pjob.pro->set_value(result);
            }
            this_thread::sleep_for(chrono::milliseconds(1000));
        }
        this_thread::yield();
    }
}
int main(){    
    thread t0(video_capture);
    thread t1(infer_worker);
    t0.join();
    t1.join();
    return 0;
}

使用std::promise 和 std::future来实现线程之间传递数据,并同步它们执行。
定义了一个Job结构体

struct Job{
    shared_ptr<promise<string>> pro;
    string input;
}

每一个job指向promise 的share_ptr智能指针和一个输入,这个输入模拟的是要处理图片,而promise 将被用来在消费者线程中设置结果,并在生产者获取这个结果。
在生产者线程中,为每个新的Job创建了一个新的promise对象。将这个job添加到队列中,等待消费者线程处理它。

job.pro.reset(new promise<string>());
...
auto result = job.pro->get_future().get();
printf("Job %s -> %s\n", job.input.c_str(), result.c_str());

当消费者处理一个Job并设置了结果后,生产者线程使用job.pro->get_future().get()来等待这个结果。这会阻塞生产者线程,直到消费者线程设置了promise的值。
在消费者线程中,处理了一个Job后,它创建一个结果字符串,模拟模型推理结果,并使用pjob.pro->set_value(result);设置promise的值。这会立即解锁在生产者线程中等待这个promise的任何future,并允许它继续执行。由于promise和future之间紧密关系,线程安全的。

五.问答

1.是的,当与 std::condition_variable 一起使用时,通常选择 std::unique_lock 而不是 std::lock_guard。这两者都可以用来管理互斥锁,但它们在功能上有所不同,尤其是在与条件变量一起使用时。std::lock_guard 不能与 std::condition_variable 一起使用,因为 std::condition_variable::wait() 需要能够临时解锁其关联的互斥量,而 lock_guard 不支持这个操作

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值