RAII+接口模式下的生产者消费者多batch实现以及封装

RAII+接口模式的生产者消费者封装,以及多batch的体现

前言:RAII(Resource Acquisition Is Initialization),也称直译为“资源获取就是初始化”。这里的生产者消费者指的是模型推理的过程,送一个图片进来,送的这个线程认为是生产者的线程;把这个图片变成推理后的结果,那就称为是消费者多batch指的是单批次多图处理,就是消费者一次拿batch个图片出来,而不是只pop一个图片出来进行推理。然后为什么要提到多batch呢?是因为GPU有个叫计算密集型的特性,也就是说计算越密集越友好。比如说给一张图推理假设需要5ms,两张图可能就只需要6ms,四张图可能只需要8ms。所以在生产者消费者封装的过程中就要考虑到GPU的特性,并且充分利用到它的特性,实现高性能,高性能的关键就是多batch的体现

这篇博文是承接上文的内容继续挖掘:使用RAII+接口模式对模型加载和推理进行封装

infer.cpp
对于生产者消费者模式,forward函数的过程就是往队列里抛任务,这里就是抛图片进来。还要注意一点就是尽量保证资源哪里分配,就在哪里释放,就在哪里使用,这样能使得程序足够的简单而不会太乱。所以worker内实现了模型的加载,使用,释放,好处就是资源全部在一个线程上。

// infer.cpp
#include "infer.hpp"
#include <mutex>
#include <thread>
#include <future>
#include <queue>
#include <string>
#include <memory>
#include <chrono>
#include <condition_variable>

using namespace std;

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

class InferImpl : public InferInterface {
public:
	// 析构函数
	virtual ~InferImpl() {
		worker_running_ = false;
		cv_.notify_one();

		if (worker_thread_.joinable()) worker_thread_.join();
	}

	bool load_model(const string& file) {
		// 尽量保证资源在哪里分配,就在哪里使用,就在哪里释放,这样不会太乱。比如这里我们就都在 worker 函数内完成。
		// 这里的pro表示是否启动成功
		promise<bool> pro;
		worker_running_ = true;
		worker_thread_ = thread(&InferImpl::worker, this, file, std::ref(pro));
		return pro.get_future().get();就是说只有在模型加载完成后才会为promise赋值,这时通过get一直等到结果为止
	}

	virtual shared_future<string> forward(string pic) override {
		// printf("正在使用 %s 进行推理.\n", context_.c_str());
		Job job;
		job.pro.reset(new promise<string>());
		job.input = pic;

		lock_guard<mutex> l(job_lock_);
		qjobs_.push(job);
		//detection push
		//face push
		//feature push
		//一次等待结果,实际上就是让detection+face+feature 并发执行

		// 一旦有任务需要推理,发送通知
		cv_.notify_one();

		// return job.pro->get_future().get();	// 不能这样直接返回模型推理的结果,因为这样会等待这个模型推理结束后才往下走,相当于还是串行,如果我们有多个模型的话就实现不了多个模型并发了。
		return job.pro->get_future();  // 这里是直接返回future对象,让外部按需要再.get()获取结果,所以这时forward的返回值类型就变成了shared_future<string>
	}

	void worker(string file, promise<bool>& pro) {
		// //启动worker函数这个线程,worker是实际执行推理的函数
		// context的加载、使用和释放都在worker内
		string context = file;
		if (context.empty()) {  // 未初始化,返回false
			pro.set_value(false);
			return;
		}
		else {  // 已初始化,返回true,之后正式开始进行推理
			pro.set_value(true);
		}

		int max_batch_size = 5;
		vector<Job> jobs;  // 拿多张图片 batch
		int batch_id = 0;//batch_id 的id,表示用不同的batch来做推理
		//作为消费者,都要加while(true)来捕获内容
		//因为我们知道创建线程就必须要join,所以如果这里不用while(worker_running_),而是while(true)的话,线程就停不下来,那join就会一直等待线程结束。
		while (worker_running_) {
			//对于生产者消费者模式,这里是充当消费者,在队列取任务并执行的过程
			// 被动等待接收通知
			unique_lock<mutex> l(job_lock_);
			cv_.wait(l, [&](){
					// true:停止等待
					return !qjobs_.empty() || !worker_running_;
					});
					
			// 是因为程序发送终止信号而退出wait的,所以break掉使得线程正常结束掉。
			if (!worker_running_) break;

			// 可以一次拿一批出来, 最大拿maxBatchSize个
			while (jobs.size() < max_batch_size && !qjobs_.empty()) {
				jobs.emplace_back(qjobs_.front());//当小于max_batch_size时继续从qjobs里取图片放进自定义的vector jobs里
				qjobs_.pop();
			}
			// 执行batch推理
			for (int i=0; i<jobs.size(); ++i) {
				auto& job = jobs[i];
				char name[100];
				sprintf(name, "%s : batch->%d[%d]", job.input.c_str(), batch_id, (int)jobs.size());
				//通过job.pro的方式把结果回调(反馈)回去
				job.pro->set_value(name);
			}
			batch_id++;
			//推理结束,把jobs清空,保证下次能继续地去拿图片数据
			jobs.clear();
			this_thread::sleep_for(chrono::milliseconds(infer_time_));
		}
		printf("释放模型: %s\n", context.c_str());
		context.clear(); // 释放模型
		printf("线程终止\n");
	}

private:
	atomic<bool> worker_running_{false}; // 表示thread是否正在运行,实现退出机制。目的是使得类对象被析构时,类函数线程正常的自动退出。因为我们知道创建线程就必须要join,否则core dumped。所以如果这里不用while(worker_running_),而是while(true)的话,线程就停不下来,那join就会一直等待线程结束。
	thread worker_thread_;
	queue<Job> qjobs_;//队列
	mutex job_lock_;//加锁,防止资源访问冲突
	condition_variable cv_;因为消费者始终是while(true),所以condition_variable 用来防止内容为空时还一直做while循环浪费资源
	int infer_time_ = 1000;
};

shared_ptr<InferInterface> create_infer(const string& file) {
	shared_ptr<InferImpl> instance(new InferImpl());
	if (!instance->load_model(file)) instance.reset();
	return instance;
}

infer.hpp

// infer.hpp
#ifndef INFER_HPP
#define INFER_HPP

#include <memory>
#include <string>
#include <future>

class InferInterface {
public:
	virtual std::shared_future<std::string> forward(std::string pic) = 0;
};

std::shared_ptr<InferInterface> create_infer(const std::string& file);

#endif

main.cpp

// main.cpp
#include "infer.hpp"

using namespace std;
int main() {

	string file = "model a";
	auto infer = create_infer(file);
	if (infer == nullptr) {
		printf("模型加载失败\n");
		return -1;
	}
	//这里就代表这个有三个任务(模型),forward函数就代表把图片push进队列,这里push了三次。执行完这三行代码后会等待所有模型的推理结果一起返回,实现多个模型的推理并发执行。
	//这里的三个forward也可以理解成单个模型同时进行三张图的推理,即我们题目所指的多batch实现,单批次多图处理。
	auto fa = infer->forward("A");
	auto fb = infer->forward("B");
	auto fc = infer->forward("C");
	
	//最后一起回收结果
	printf("%s\n", fa.get().c_str());
	printf("%s\n", fb.get().c_str());
	printf("%s\n", fc.get().c_str());


    //如果按照下面注释掉的部分的方式来进行推理的话,会每次都等待结果,无法进行单批次多图处理。
	// auto fa = infer->forward("A").get();
	// auto fb = infer->forward("B").get();
	// auto fc = infer->forward("C").get();
	// printf("%s\n", fa.c_str());
	// printf("%s\n", fb.c_str());
	// printf("%s\n", fc.c_str());

	printf("程序终止\n");
	return 0;
}

Refer: RAII 资源获取即初始化

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值