应用场景
我们在多线程下开发时,经常会遇到这样一个场景,有一种资源,它是有限的n个,每个资源每次只能由一个线程独占使用。
就好像一个公共厕所,蹲坑是有限的,对于无限多个要上厕所的人来说,如果蹲坑满了大家只能排队上厕所,如果大家都不守规矩要抢着上厕所,那么门口就得站个管理员来维持秩序,每从当厕所中有空闲的坑位时,就放一个人进来,指定他用这个空闲的坑位,等再有坑位空出来就再放一个人进来,否则铁定乱套。
这里,这个厕所管理员就好比一个资源管理器,管理着所有资源,所有的线程都要向这个管理员申请才能获取所要的资源。
完整代码
下面代实现的resource_manager,就是基于这个应用需求实现的c++11模板类。
/*
* resource_manager.h
*
* Created on: 2016年7月25日
* Author: guyadong
*/
#ifndef COMMON_SOURCE_CPP_RESOURCE_MANAGER_H_
#define COMMON_SOURCE_CPP_RESOURCE_MANAGER_H_
#include <thread>
#include <type_traits>
#include <memory>
#include <vector>
#include <queue>
#include <utility>
#include <initializer_list>
#include "assert_macros.h"
#include "threadsafe_queue.h"
#include "threadsafe_unordered_map.h"
#include "raii.h"
namespace gdface {
/*
* 多线程类空间
*/
inline namespace mt{
/*
* 无资源异常
* 当资源数为0时抛出此异常时
* */
class no_resource_except :public std::logic_error {
public:
// 继承基类构造函数
using std::logic_error::logic_error;
no_resource_except(const std::string &at, const std::string &msg) :logic_error(std::string(at).append(":").append(msg)) {}
no_resource_except(const std::string &at, const std::exception&e) :logic_error(std::string(at).append(":").append(e.what())) {}
no_resource_except(const std::string &at, const std::exception&e, const std::string &msg) :logic_error(std::string(at).append(":").append(e.what()).append(":").append(msg)) {}
};
/*
* 多线程环境共享资源管理类
* 禁止移动/复制构造函数
* 禁止移动/复制赋值操作符
* 所有被管理的资源(R)存放在数组中
* acquire申请资源,当无资源可用时阻塞
* release释放资源
* 同一线程内允许嵌套执行acquire/release
* acquire/release必须配对使用,否则会造成资源泄漏
* */
template<typename R>
class resource_manager {
private:
// 资源索引类型
using resource_index_type=size_t;
// 资源队列类型
using resource_queue_type=threadsafe_queue<resource_index_type>;
public:
// 返回类型,R为标量类型时直接返回R的值,否则返回引用
using return_type=typename std::conditional<std::is_scalar<R>::value,R,R&>::type;
resource_manager()=default;
/* 禁止复制构造 */
resource_manager(const resource_manager&)=delete;
/* 禁止移动构造 */
resource_manager(resource_manager&&)=delete;
/* 禁止复制赋值 */
resource_manager&operator=(const resource_manager&)=delete;
/* 禁止移动赋值 */
resource_manager&operator=(resource_manager&&)=delete;
/*
* 基本构造函数,
* 使用迭代器为参数的构造函数,适用所有容器对象,
* lock_count初始化为0
* occupy_map初始化为空
* free_queue中包含所有资源索引
* */
template<typename _InputIterator>
resource_manager(_InputIterator first, _InputIterator last) :
resource(first, last)
,lock_count(resource.size(), typename decltype(lock_count)::value_type(0))
,occupy_thread()
,free_queue(make_free_queue(resource.size())){
}
/*
* 对于类型为整数的资源,提供一个简便的构造函数
* count 资源数目
* start 整数起始值
* 根据这两个参数构建一个start开始count个整数作为资源数组
* */
template<typename Enable=typename std::enable_if<std::is_integral<R>::value>::type>
resource_manager(size_t count,R start=0):resource_manager(make_vector<R>(count,start)){}
/*
* std::vector类型的资源数组为参数的构造函数
* */
resource_manager(const std::vector<R>& res):resource_manager(res.begin(),res.end()){}
/*
* 使用初始化列表为参数的构造函数
* */
resource_manager(std::initializer_list<R> list):resource_manager(list.begin(),list.end()){}
virtual ~resource_manager(){
// 将资源数组清空,如果还有线程请求资源会导致抛出no_resource_except异常
resource.clear();
}
/*
* 返回一个自动化的资源管理对象(不可跨线程使用)
* raii_var对象构造时会自动申请资源
* raii_var对象析构时会自动释放资源
* raii_var对象的生命周期必须在当前对象生命周期内,否则在执行资源释放时this指针无效
* */
raii_var<return_type> resource_guard(){
return raii_var<return_type>(
[this]{return this->acquire();},
[this](return_type){this->release();}
);
}
private:
std::vector<R> resource;
// 占用资源的线程中的加锁计数
std::vector<size_t> lock_count;
// 保存每个占用资源的线程id和所占用资源索引的映射,初始为空
threadsafe_unordered_map<std::thread::id,resource_index_type> occupy_thread;
// 空闲资源(索引)队列,队列中保存的是资源在resource中的索引,初始为resource全部索引
std::shared_ptr<resource_queue_type> free_queue;
template<typename Enable=typename std::enable_if<std::is_integral<R>::value>::type>
static std::vector<R>
make_vector(size_t count,R start=0){
std::vector<R> v(count);
for(size_t i=0;i<count;++i){
v[i]=R(i+start);
}
return v;
}
/*
* 创建并初始化资源索引队列,将所有资源索引加入队列
* */
static std::shared_ptr<resource_queue_type>
make_free_queue(size_t size) {
using v_type = typename resource_queue_type::value_type;
std::vector<v_type>v(size);
//创建索引数组
for (size_t i = 0; i<size; ++i) { v[i] = v_type(i); }
return std::make_shared<resource_queue_type>(v.begin(), v.end());;
}
/*
* 阻塞方式从队列中获取可用的资源
* 资源数为0时抛出no_resource_except异常
*/
return_type acquire(){
auto this_thread_id=std::this_thread::get_id();
resource_index_type resource_index;
// 当前线程重复加锁时不需要再申请资源,将加lock_cout+1,然后返指定的对象
occupy_thread.insertIfAbsent(this_thread_id,resource_index);
occupy_thread.replace(this_thread_id,resource_index);
occupy_thread.replace(this_thread_id,resource_index,resource_index);
if(!occupy_thread.find(this_thread_id,resource_index)){
// 向空闲队列申请资源
resource_index=free_queue->wait_and_pop();
// 状态不对常抛出异常throw_except_if_msg(std::logic_error,0>resource_index||0!=lock_count[resource_index],"invalid resource status");
// 将申请到的资源索引加入线程占用表,代表当前线程已经使用了这个资源
occupy_thread.insert({this_thread_id,resource_index});
}
++lock_count[resource_index];
// 资源数目为0时抛出异常
throw_except_if(no_resource_except,resource.empty());
return resource[resource_index];
}
/*
* 释放资源
*/
void release(){
resource_index_type resource_index;
auto thread_id=std::this_thread::get_id();
// 状态不对常抛出异常
throw_except_if_msg(std::logic_error,
!occupy_thread.find(thread_id,resource_index)||resource_index>=lock_count.size()||lock_count[resource_index]<=0
,"invalid acquire/release nest");
--lock_count[resource_index];
// 当前线程所有嵌套解锁后才将资源重新加入free_queue
if(0==lock_count[resource_index]){
// 从线程map中删除当前线程
occupy_thread.erase(thread_id);
// 将释放出来的索引号加入free队列
free_queue->push(resource_index);
}
}
};
}/* namespace mt */
} /* namespace gdface */
#endif /* COMMON_SOURCE_CPP_RESOURCE_MANAGER_H_ */
原理说明
这个类的主要原理是将资源®存储在数组(std::vector
)中,用一个队列(threadsafe_queue
)来管理所有空闲的资源索引(free_queue
)。
线程每次调用acquire
函数从空闲资源队列(free_queue
)中获取一个资源,如果队列为空就阻塞。
线程使用完资源后调用release
函数将资源重回加入队列,并唤醒等待资源的线程。
同一个线程多次调用acquire
不会重复申请资源,只会将已经申请的资源对应的引用计数(lock_count
)加1,同一个线程多次调用release
不会重复释放资源,只会将已经申请的资源对应的引用计数(lock_count
)减1,直到计数器为0再将资源放回空闲资源队列。
代码中用于管理空闲资源的队列用到的threadsafe_queue类参见我之前的博客《C++11:基于std::queue和std::mutex构建一个线程安全的队列》
代码中用到的threadsafe_unordered_map参见我之前的博客《C++11:基于std::unordered_map和共享锁构建线程安全的map》
代码用到的raii类参见我之前的博客《C++11实现模板化(通用化)RAII机制》
使用示例
类中除了构造函数之外只有一个公开的resource_guard
函数。这个函数返回的raii
类自动完成了资源申请和释放的动作,所以资源的使用非常简单,调用代码根本不用关心资源的申请和释放。
下面是代码示例片段
using channel_type =short;
using channel_manger_type=mt::resource_manager<channel_type>;
// 人脸检测通道管理器
std::unique_ptr<channel_manger_type> detect_channel_mgr;
JNIEXPORT jobjectArray JNICALL Java_net_facesdk_cas_JNIBridge_detectFace
(JNIEnv *env, jclass, jbyteArray jImgData, jint bpp, jint nWidth, jint nHeight){
....
// 调用resource_guard函数获取通道资源
// 资源的申请和释放都已经被RAII对象自动完成了,
// 这里调用代码只管调用get函数返回所要的资源就行了,不用考虑资源申请和释放的问题
auto face_num=THFI_DetectFace(detect_channel_mgr->resource_guard().get(), (BYTE*)(*raii_byte_ptr), bpp, nWidth, nHeight, get_face_pos_buf().get().data(), int(get_face_pos_buf().get().size()));
...
}