在基于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_DpOT0
llvm源码:
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工具箱再添一员猛将!
我手头其实只有4.8.5和10.2的gcc编译器,我10.2的gcc编译是没问题的。这里之所以说gcc版本8以上就能支持,是我在 https://godbolt.org 上给pthread_create做类似封装,测试得出的结论,如果大家实际测试8.1或10.2以下版本编译器编译不过的话,欢迎反馈。