一.简单的生产者消费者模式
- 这是用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 不支持这个操作