部署中多线程常用组件:thread, join, joinable, detach, mutex, promise, future, condition_variable, 生产者消费者模式
一、模拟部署中的生产者消费者
1. 最简单实现
#include <iostream>
#include <thread>
#include <chrono>
#include <queue>
#include <mutex>
#include <string>
#include <stdio.h>
using namespace std;
queue<string> qjobs_;
void video_capture() {
int pic_id = 0;
while (true) {
char name[100];
sprintf_s(name, "PIC-%d", pic_id++);
printf("生成了一个新图片:%s\n", name);
qjobs_.push(name);
//读图1s
this_thread::sleep_for(chrono::milliseconds(1000));
}
}
void infer_worker() {
while (true) {
if (!qjobs_.empty()) {
auto pic = qjobs_.front();
qjobs_.pop();
printf("消耗掉一个图片:%s\n", pic.c_str());
//推理1s
this_thread::sleep_for(chrono::milliseconds(1000));
}
this_thread::yield(); //交出cpu控制权,进入空闲状态,继续等待下一个时间片
}
}
int main() {
thread t0(video_capture);
thread t1(infer_worker);
t0.join();
t1.join();
return 0;
}
2. queue,stl对象不是thread-safe,需要加锁
#include <iostream>
#include <thread>
#include <chrono>
#include <queue>
#include <mutex>
#include <string>
#include <stdio.h>
using namespace std;
queue<string> qjobs_;
mutex lock_;
void video_capture() {
int pic_id = 0;
while (true) {
{
lock_guard<mutex> l(lock_); //作用域在大括号{},生产过程枷锁,超过作用域析构自动解锁
char name[100];
sprintf_s(name, "PIC-%d", pic_id++);
printf("生成了一个新图片:%s\n", name);
qjobs_.push(name);
}//自动解锁
//读图1s
this_thread::sleep_for(chrono::milliseconds(1000));
}
}
void infer_worker() {
while (true) {
if (!qjobs_.empty()) {
{
//lock_guard作用域在大括号{},消费过程枷锁,数据从队列一瞬间加锁,超过作用域析构自动解锁
//推理等待过程不加锁,不然无法填充
lock_guard<mutex> l(lock_);
auto pic = qjobs_.front();
qjobs_.pop();
printf("消耗掉一个图片:%s\n", pic.c_str());
}
//推理1s
this_thread::sleep_for(chrono::milliseconds(1000));
}
this_thread::yield(); //交出cpu控制权,进入空闲状态,继续等待下一个时间片
}
}
int main() {
thread t0(video_capture);
thread t1(infer_worker);
t0.join();
t1.join();
return 0;
}
二、队列溢出问题:生产太快,消费太慢
1. 生产速度大小消费速度造成队列堆积现象
#include <iostream>
#include <thread>
#include <chrono>
#include <queue>
#include <mutex>
#include <string>
#include <stdio.h>
using namespace std;
queue<string> qjobs_;
mutex lock_;
void video_capture() {
int pic_id = 0;
while (true) {
{
lock_guard<mutex> l(lock_); //作用域在大括号{},生产过程枷锁,超过作用域析构自动解锁
char name[100];
sprintf_s(name, "PIC-%d", pic_id++);
printf("生成了一个新图片:%s, qjobs_size = %d\n", name, qjobs_.size());
qjobs_.push(name);
}//自动解锁
//读图1s
this_thread::sleep_for(chrono::milliseconds(500));
}
}
void infer_worker() {
while (true) {
if (!qjobs_.empty()) {
{
//lock_guard作用域在大括号{},消费过程枷锁,数据从队列一瞬间加锁,超过作用域析构自动解锁
//推理等待过程不加锁,不然无法填充
lock_guard<mutex> l(lock_);
auto pic = qjobs_.front();
qjobs_.pop();
printf("消耗掉一个图片:%s\n", pic.c_str());
}
//推理1s
this_thread::sleep_for(chrono::milliseconds(1000));
}
this_thread::yield(); //交出cpu控制权,进入空闲状态,继续等待下一个时间片
}
}
int main() {
thread t0(video_capture);
thread t1(infer_worker);
t0.join();
t1.join();
return 0;
}
打印结果:
例子使用的是string,但实际推理中queue
中存的是图片,如果图片大小是1280 * 720 * 3,容易显存消耗殆尽,程序崩溃
2. 解决方法:设置生产上限
- 达到生产上限停止生产,等待消费者推理完成,队列有空间再生产
- 使用
condition_variable
,传的参数是unique_lock
- 流程:判断队列的数量是否超过了limit,如果超过,则一直
wait()
,且此时解锁,直到消费掉一个,使用notice_one()
通知生产者,跳出wait()
,同时生产者加锁
#include <iostream>
#include <thread>
#include <chrono>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <string>
#include <stdio.h>
using namespace std;
queue<string> qjobs_;
mutex lock_;
condition_variable cv_;
const int limit_ = 5;
void video_capture() {
int pic_id = 0;
while (true) {
{
unique_lock<mutex> l(lock_); //作用域在大括号{},生产过程枷锁,超过作用域析构自动解锁
char name[100];
sprintf_s(name, "PIC-%d", pic_id++);
printf("生成了一个新图片:%s, qjobs_size = %d\n", name, qjobs_.size());
//if (qjobs_.size() > limit)
// wait();
cv_.wait(l, [&]() {
//return false:继续等待
//return true:等待结束,跳出wait
//进入wait,解锁
//跳出wait,加锁
return qjobs_.size() < limit_;
});
qjobs_.push(name);
// 如果队列满了不生产,等待队列有空间再生产
// 通知的问题,如果即使通知到wait,让他随时可以退出
}//自动解锁
//读图1s
this_thread::sleep_for(chrono::milliseconds(500));
}
}
void infer_worker() {
while (true) {
if (!qjobs_.empty()) {
{
//lock_guard作用域在大括号{},消费过程枷锁,数据从队列一瞬间加锁,超过作用域析构自动解锁
//推理等待过程不加锁,不然无法填充
lock_guard<mutex> l(lock_);
auto pic = qjobs_.front();
qjobs_.pop();
//消费掉一个,通知wait(),跳出等待
cv_.notify_one();
printf("消耗掉一个图片:%s\n", pic.c_str());
}
//推理1s
this_thread::sleep_for(chrono::milliseconds(1000));
}
this_thread::yield(); //交出cpu控制权,进入空闲状态,继续等待下一个时间片
}
}
int main() {
thread t0(video_capture);
thread t1(infer_worker);
t0.join();
t1.join();
return 0;
}
打印结果:队列数量不超过limit = 5
三、生产者拿到消费者的反馈
推理过程中,有很多种模型,比如目标检测、车道线检测、深度估计等,即有很多个消费者,多个消费者异步推理,消费者推理完图片得到的结果要反馈给生产者,进行后处理和画框等
同步模式: detection -> infer, face -> infer, feature -> infer
异步模式: detaction -> push, face -> push, feature -> push 一次进行三个结果的回收,然后处理
#include <iostream>
#include <thread>
#include <chrono>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <future>
#include <memory>
#include <string>
#include <stdio.h>
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 pic_id = 0;
while (true) {
Job job;
{
unique_lock<mutex> l(lock_); //作用域在大括号{},生产过程枷锁,超过作用域析构自动解锁
char name[100];
sprintf_s(name, "PIC-%d", pic_id++);
printf("生成了一个新图片:%s, qjobs_size = %d\n", name, qjobs_.size());
//if (qjobs_.size() > limit)
// wait();
cv_.wait(l, [&]() {
//return false:继续等待
//return true:等待结束,跳出wait
//进入wait,解锁
//跳出wait,加锁
return qjobs_.size() < limit_;
});
// 如果队列满了不生产,等待队列有空间再生产
// 通知的问题,如果即使通知到wait,让他随时可以退出
job.pro.reset(new promise<string>());
job.input = name;
qjobs_.push(job);
}//自动解锁
//等待这个job处理完毕,拿到结果
//.get过后,实现等待,知道promise->set_value执行了,返回值就是result
auto result = job.pro->get_future().get();
printf("JOB %s -> %s\n", job.input.c_str(), result.c_str());
//拿到推理结果,几个推理结果一起画框
//读图1s
this_thread::sleep_for(chrono::milliseconds(500));
}
}
void infer_worker() {
while (true) {
if (!qjobs_.empty()) {
{
//lock_guard作用域在大括号{},消费过程枷锁,数据从队列一瞬间加锁,超过作用域析构自动解锁
//推理等待过程不加锁,不然无法填充
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);
}
//推理1s
this_thread::sleep_for(chrono::milliseconds(1000));
}
this_thread::yield(); //交出cpu控制权,进入空闲状态,继续等待下一个时间片
}
}
int main() {
thread t0(video_capture);
thread t1(infer_worker);
t0.join();
t1.join();
return 0;
}
打印结果:
不需要condition_variable
了
#include <iostream>
#include <thread>
#include <chrono>
#include <queue>
#include <mutex>
#include <future>
#include <memory>
#include <string>
#include <stdio.h>
using namespace std;
struct Job {
shared_ptr<promise<string>> pro;
string input;
};
queue<Job> qjobs_;
mutex lock_;
const int limit_ = 5;
void video_capture() {
int pic_id = 0;
while (true) {
Job job;
{
lock_guard<mutex> l(lock_); //作用域在大括号{},生产过程枷锁,超过作用域析构自动解锁
char name[100];
sprintf_s(name, "PIC-%d", pic_id++);
printf("生成了一个新图片:%s, qjobs_size = %d\n", name, qjobs_.size());
job.pro.reset(new promise<string>());
job.input = name;
qjobs_.push(job);
}//自动解锁
//等待这个job处理完毕,拿到结果
//.get过后,实现等待,知道promise->set_value执行了,返回值就是result
auto result = job.pro->get_future().get();
printf("JOB %s -> %s\n", job.input.c_str(), result.c_str());
//拿到推理结果,几个推理结果一起画框
//读图1s
this_thread::sleep_for(chrono::milliseconds(500));
}
}
void infer_worker() {
while (true) {
if (!qjobs_.empty()) {
{
//lock_guard作用域在大括号{},消费过程枷锁,数据从队列一瞬间加锁,超过作用域析构自动解锁
//推理等待过程不加锁,不然无法填充
lock_guard<mutex> l(lock_);
auto pjob = qjobs_.front();
qjobs_.pop();
printf("消耗掉一个图片:%s\n", pjob.input.c_str());
auto result = pjob.input + "-----------infer";
//new_pic返回生产者
pjob.pro->set_value(result);
}
//推理1s
this_thread::sleep_for(chrono::milliseconds(1000));
}
this_thread::yield(); //交出cpu控制权,进入空闲状态,继续等待下一个时间片
}
}
int main() {
thread t0(video_capture);
thread t1(infer_worker);
t0.join();
t1.join();
return 0;
}
多个推理模型的模拟(一个生产者多个消费者):
#include <iostream>
#include <thread>
#include <chrono>
#include <queue>
#include <mutex>
#include <future>
#include <memory>
#include <string>
#include <stdio.h>
using namespace std;
struct Job {
shared_ptr<promise<string>> pro;
string input;
};
queue<Job> qjobs1_;
queue<Job> qjobs2_;
queue<Job> qjobs3_;
mutex lock_;
// 图像处理函数示例:目标检测
string detect(const string& image) {
// 模拟目标检测的处理时间
this_thread::sleep_for(chrono::milliseconds(1000));
cout << "目标检测完成" << endl;
return "目标检测结果";
}
// 图像处理函数示例:车道线检测
string laneDetection(const string& image) {
// 模拟车道线检测的处理时间
this_thread::sleep_for(chrono::milliseconds(1500));
cout << "车道线检测完成" << endl;
return "车道线检测结果";
}
// 图像处理函数示例:深度估计
string depthEstimation(const string& image) {
// 模拟深度估计的处理时间
this_thread::sleep_for(chrono::milliseconds(2000));
cout << "深度估计完成" << endl;
return "深度估计结果";
}
void video_capture() {
int pic_id = 0;
while (true) {
Job job1;
Job job2;
Job job3;
{
lock_guard<mutex> l(lock_); //作用域在大括号{},生产过程枷锁,超过作用域析构自动解锁
char name[100];
sprintf_s(name, "PIC-%d", pic_id++);
printf("生成了一个新图片:%s, qjobs_size = %d\n", name, qjobs1_.size());
job1.pro.reset(new promise<string>());
job1.input = name;
job2.pro.reset(new promise<string>());
job2.input = name;
job3.pro.reset(new promise<string>());
job3.input = name;
qjobs1_.push(job1);
qjobs2_.push(job2);
qjobs3_.push(job3);
}//自动解锁
//等待这个job处理完毕,拿到结果
//.get过后,实现等待,知道promise->set_value执行了,返回值就是result
auto result1 = job1.pro->get_future().get();
auto result2 = job2.pro->get_future().get();
auto result3 = job3.pro->get_future().get();
printf("JOB %s -> %s\n", job1.input.c_str(), result1.c_str());
printf("JOB %s -> %s\n", job2.input.c_str(), result2.c_str());
printf("JOB %s -> %s\n", job3.input.c_str(), result3.c_str());
//拿到推理结果,几个推理结果一起画框
//读图1s
this_thread::sleep_for(chrono::milliseconds(2000));
}
}
void infer_worker1() {
while (true) {
if (!qjobs1_.empty()) {
Job pjob;
{
//lock_guard作用域在大括号{},消费过程枷锁,数据从队列一瞬间加锁,超过作用域析构自动解锁
//推理等待过程不加锁,不然无法填充
lock_guard<mutex> l(lock_);
pjob = qjobs1_.front();
qjobs1_.pop();
printf("消耗掉一个图片:%s\n", pjob.input.c_str());
}
//目标检测
string detectionResult = detect(pjob.input);
auto result = pjob.input + "----------" + detectionResult;
//new_pic返回生产者
pjob.pro->set_value(result);
}
this_thread::yield(); //交出cpu控制权,进入空闲状态,继续等待下一个时间片
}
}
void infer_worker2() {
while (true) {
if (!qjobs2_.empty()) {
Job pjob;
{
//lock_guard作用域在大括号{},消费过程枷锁,数据从队列一瞬间加锁,超过作用域析构自动解锁
//推理等待过程不加锁,不然无法填充
lock_guard<mutex> l(lock_);
pjob = qjobs2_.front();
qjobs2_.pop();
printf("消耗掉一个图片:%s\n", pjob.input.c_str());
}
//车道线检测
string laneDetectionResult = laneDetection(pjob.input);
auto result = pjob.input + "----------" + laneDetectionResult;
//new_pic返回生产者
pjob.pro->set_value(result);
}
this_thread::yield(); //交出cpu控制权,进入空闲状态,继续等待下一个时间片
}
}
void infer_worker3() {
while (true) {
if (!qjobs3_.empty()) {
Job pjob;
{
//lock_guard作用域在大括号{},消费过程枷锁,数据从队列一瞬间加锁,超过作用域析构自动解锁
//推理等待过程不加锁,不然无法填充
lock_guard<mutex> l(lock_);
pjob = qjobs3_.front();
qjobs3_.pop();
printf("消耗掉一个图片:%s\n", pjob.input.c_str());
}
//深度估计
string depthEstimationResult = depthEstimation(pjob.input);
auto result = pjob.input + "----------" + depthEstimationResult;
//new_pic返回生产者
pjob.pro->set_value(result);
}
this_thread::yield(); //交出cpu控制权,进入空闲状态,继续等待下一个时间片
}
}
int main() {
thread t0(video_capture);
thread t1(infer_worker1);
thread t2(infer_worker2);
thread t3(infer_worker3);
t0.join();
t1.join();
t2.join();
t3.join();
return 0;
}