把bthread_start_background封装成现代C++的风格!

2021-12-08 13:32:11 浏览数 (1)

在基于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()相似函数参数。

代码语言:javascript复制
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来中转。比如,我们有一个函数:

代码语言:javascript复制
void foo(int a, int b, std::string s);

想要在bthread中执行,只能这样:

代码语言:javascript复制
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,却可以不用这么麻烦,它可以直接:

代码语言:javascript复制
    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以上

代码不多直接看代码:

代码语言:javascript复制
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函数,就是能直接把参数打散来调用。比如这样:

代码语言:javascript复制
    bthread_t th;
    call_bthread(th, NULL, echo, "hello brpc");
    bthread_join(th, NULL);

怎么样,是不是方便很多了呢?

也许你会问,std::thread可是一个类啊,你这里能不能封装成一个类呢?可以,下面我来演示一下。

由于bthread_start_background是需要接收属性参数的,而std::thread不需要,所以我实现的这个类会额外多一个属性参数,需要外部传入。另外第一个参数bthread_t类型的id,其实我们一般是不关心的,所以就不传入了。

直接看代码:

代码语言:javascript复制
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任务啦!

代码语言:javascript复制
    Bthread bt(NULL, echo, "hello brpc");
    bt.join();

如果你工作的编译器版本是8.1及以上的话,完全可以把这段代码放到项目中,让你的brpc工具箱再添一员猛将!

0 人点赞