在基于brpc开发服务的时候,bthread_start_background()
一定是高频函数。bthread_start_background()是brpc框架提供给我们的API,让我们可以方便使用brpc的协程bthread。
然而在brpc的设计思想中,bthread_start_background()需要和pthread_create()兼容,在某些情况下直接用pthread_create()来执行bthread的回调函数。所以bthread_start_background(是声明在extern "C"中。并且有和POSIX的C标准函数pthread_create()相似函数参数。
int pthread_create(pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine) (void *),
void *arg);
int bthread_start_background(bthread_t* __restrict tid,
const bthread_attr_t* __restrict attr,
void * (*fn)(void*),
void* __restrict args);
它们两个的第三个参数,即线程/协程回调函数的类型是完全一样的。回调函数类型必须是参数为void*,返回值也为void*的函数。所以如果我们想执行的函数是多个参数的只能通过struct来中转。比如,我们有一个函数:
void foo(int a, int b, std::string s);
想要在bthread中执行,只能这样:
struct Args {
int a;
int b;
std::string s;
};
void* call_back(void* ori_args) {
Args* args = (Args*)ori_args;
foo(args->a, args->b, args->s);
delete args;
return nullptr;
}
// 在需要调用的地方
...
Args* args = new Args;
// a、b、s是已有的int、int和string类型变量
args->a = a;
args->b = b;
args->s = s;
bthread_t id;
bthread_start_background(&id, NULL, call_back, (void*)args);
类似上述代码,我和同事在工作中还出过几次bug,比如在回调函数中漏了delete
(把delete操作放到了某个if条件中)或者写成了delete ori_args;
从而导致了内存泄露。当然问题也不难排查,不过还是浪费时间,而且这个API用起来也不方便。
回想起C++11使用到std::thread,却可以不用这么麻烦,它可以直接:
std::thread(foo, a, b, s);
并且foo可以是任意的callable类型,不仅是函数,还能是lambda,函数对象、std::bind的返回值等。
那么bthread能封装成类似的不经过void*
中转的API么?
答案是能。
因为std::thread在Linux/Unix环境上其实也是对pthread的封装。其本质通过tuple的中转来实现的,所以我们的需求理论上也是能实现的,不过代码还是比较多,涉及到很多模板元编程的知识,过于复杂了。
可以看下编译器实现的std::thread的源码:
gcc源码:
https://code.woboq.org/gcc/libstdc++-v3/include/std/thread.html#ZNSt6threadC1EOT_DpOT0llvm源码:
https://github.com/llvm-mirror/libcxx/blob/master/include/thread#L287那有没有一种更为简洁的实现方案呢?
答案是有。
不过需要编译器的版本比较高,但gcc版本需要8以上
。
代码不多直接看代码:
template<class Fn, class... Args>
void call_bthread(bthread_t& th, const bthread_attr_t* attr, Fn&& fn, Args&&... args) {
auto p_wrap_fn = new auto([=]{ fn(args...); });
auto call_back = [](void* ar) ->void* {
auto f = reinterpret_cast<decltype(p_wrap_fn)>(ar);
(*f)();
delete f;
return nullptr;
};
bthread_start_background(&th, attr, call_back, p_wrap_fn);
}
这里定义的call_bthread函数,就是能直接把参数打散来调用。比如这样:
bthread_t th;
call_bthread(th, NULL, echo, "hello brpc");
bthread_join(th, NULL);
怎么样,是不是方便很多了呢?
也许你会问,std::thread可是一个类啊,你这里能不能封装成一个类呢?可以,下面我来演示一下。
由于bthread_start_background是需要接收属性参数的,而std::thread不需要,所以我实现的这个类会额外多一个属性参数,需要外部传入。另外第一个参数bthread_t类型的id,其实我们一般是不关心的,所以就不传入了。
直接看代码:
class Bthread {
public:
template<class Fn, class... Args>
Bthread(const bthread_attr_t* attr, Fn&& fn, Args&&... args) {
auto p_wrap_fn = new auto([=]{ fn(args...); });
auto call_back = [](void* ar) ->void* {
auto f = reinterpret_cast<decltype(p_wrap_fn)>(ar);
(*f)();
delete f;
return nullptr;
};
bthread_start_background(&th_, attr, call_back, (void*)p_wrap_fn);
joinable_ = true;
}
void join() {
if (joinable_) {
bthread_join(th_, NULL);
joinable_ = false;
}
}
bool joinable() const noexcept {
return joinable_;
}
bthread_t get_id() {
return th_;
}
private:
bthread_t th_;
bool joinable_ = false;
};
这里我实现了一个类Bthread,里面实现了几个简单的接口,当然关于其他构造函数与运算符或支持或禁止等细节,这里没做考虑(不是本文重点)。
有了这个Bthread类后,我们就可以这样创建bthread任务啦!
Bthread bt(NULL, echo, "hello brpc");
bt.join();
如果你工作的编译器版本是8.1及以上的话,完全可以把这段代码放到项目中,让你的brpc工具箱再添一员猛将!