大数跨境
0
0

C++11是如何封装Thread库的?

C++11是如何封装Thread库的? CppGuide
2023-04-20
0
导读:绝大多数OS的thread入口函数大致是这样的:void_or_error_code entry_point

绝大多数OS的thread入口函数大致是这样的:

void_or_error_code entry_point(void *arbitrary_data);

而std::thread的构造函数长这样:

template< class Function, class... Args > 
explicit thread( Function&& f, Args&&... args );

为了填平两者之间的差异,我们要做的就是把f和args统统打包在一起做成一个void *,然后用一个预定义的plain C function作为entry point,接收这个void *,解开其中的f和args,然后调用。

和某些说法不同,OS thread API不能直接触碰function template,因为它们大都长这样(略去不相关的参数):

error_code create_thread((void_or_error_code(*entry)(void *), void *data);

不用折腾了,你想破头也没办法让这个API直接调用一个function template或者一个lambda,你只能给它提供一个plain function pointer。用std::function<void_or_error()>绕圈子是个办法,但到最后你还是需要把f和args打包在一起,然后再用一个包装函数拆包调用f(args...),然后再把这个函数包装在std::function里,我觉得反倒更麻烦了。

下面一步一步细说:

1、打包f和args成一个void *

当然我们不能真得把f和args直接变成一个void *,至少尺寸肯定不合适,所以我们需要一个数据结构来保存f和arg。另外,我们说过thread entry point必须是一个plain function,所以template戏法对于entry point来说是不能用的,为了让我们的entry point可以使用这些数据,我们需要一个concrete type而不是template。

不得不承认virtual function有时候还是有用的。让我们定义一个基类:

struct thread_data_base
{
    virtual ~thread_data_base(){}
    virtual void run()=0;
};

这个基类给我们提供了一个统一的入口,可以调用实际用户提供的f和args。

2、接下来我们定义一个template,以适配不同类型的f和args:

template<typename F, class... ArgTypes>
class thread_data : public thread_data_base
{
public:
    thread_data(F&& f_, ArgTypes&&... args_)
    : fp(std::forward<F>(f_), std::forward<ArgTypes>(args_)...)
    {}

    template <std::size_t... Indices>
    void run2(tuple_indices<Indices...>)
    { invoke(std::move(std::get<0>(fp)), std::move(std::get<Indices>(fp))...); }

    void run() {
        typedef typename make_tuple_indices<std::tuple_size<std::tuple<F, ArgTypes...> >::value, 1>::type index_type;
        run2(index_type());
    }
    
private:
    /// Non-copyable
    thread_data(const thread_data&)=delete;
    void operator=(const thread_data&)=delete;
    std::tuple<typename std::decay<F>::type, typename std::decay<ArgTypes>::type...> fp;
};

在这个template里有一个data member,它是一个tuple,用于保存f和args,这样我们就可以通过将void *data cast成thread_data_base *,然后调用其中的虚函数run来实际调用f(args...),里面的invoke和tuple_indices等下再说。

3、然后我们就可以用一个简单的函数把任意的f和args包装成一个thread_data_base *:

template<typename F, class... ArgTypes>
inline thread_data_base *make_thread_data(F&& f, ArgTypes&&... args)
{
    return new thread_data<typename std::remove_reference<F>::type, ArgTypes...>(std::forward<F>(f), 
      std::forward<ArgTypes>(args)...);
}

4、thread entry point变得很简单,这样就行了:

void_or_error_code thread_entry(void *data) {
    std::unique_ptr<thread_data_base> p((thread_data_base *)data);
    p->run();
    // return result of p->run() if error code is required
}

异常处理等细节略去。

注意:下节含有大量C++黑魔法,可能会引起阅读者不适,请谨慎前行。

5、下面看看invoke,或者说如何通过一个f和args组成的tuple调用f(args...)

简单说,对于一个tuple<F, T1, T2, T3> tp(f, a1, a2, a3),如果你想调用f(a1, a2, a3),你需要:

tp.get<0>()(tp.get<1>(), tp.get<2>(), tp.get<3>());

forward和decay神马的略去不提。注意这里的1、2、3必须是编译期常数,否则你是没法拿来当template参数的,也就是说,为了调用f(args...),我们必须生成一个编译期的数列,这个编译期的数列就是前面提到的tuple_indices。有了这个数列,假如它叫Indices,我们就可以这样调用:tp.get<0>()(tp.get ()...); make_tuple_indices就是用来生成Indices的。为了生成数列[Sp, Ep),我们要做的就是从Sp开始,递归地在已有数列后面加一项,直到满足条件(Sp==Ep)。

让我们先定义tuple_indices:

template <std::size_t...> struct tuple_indices {};

这个类不需要任何成员,所有需要的信息,也就是整个数列,都是template参数,是类型的一部分。为了让代码清楚一点,我们再绕个圈子:

template <std::size_t Sp, class IntTuple, std::size_t Ep> struct make_indices_imp;

之所以要绕这个圈子,是因为我们的make_tuple_indices应该长这样:

template <std::size_t Ep, std::size_t Sp>
struct make_tuple_indices {...};

但在生成这个数列的过程中,为了方便,我们想把数列当前项直接放在参数列表里,要不然还需要在内部找到数列的最后一项,太烦。这个make_tuple_indices_imp完工后是这个样子的:

template <std::size_t Sp, class IntTuple, std::size_t Ep> struct make_indices_imp;

template <std::size_t Sp, std::size_t... Indices, std::size_t Ep>
struct make_indices_imp<Sp, tuple_indices<Indices...>, Ep>
{ typedef typename make_indices_imp<Sp+1, tuple_indices<Indices..., Sp>, Ep>::type type; };

template <std::size_t Ep, std::size_t... Indices>
struct make_indices_imp<Ep, tuple_indices<Indices...>, Ep>
{ typedef tuple_indices<Indices...> type; };

可以看到有三个版本,第一个是泛化形式,第二个是递归中间结果,第三个是递归终止条件(Sp==Ep)然后我们就可以把make_tuple_indices写出来了:

template <std::size_t Ep, std::size_t Sp=0>
struct make_tuple_indices {
    typedef typename make_indices_imp<Sp, tuple_indices<>, Ep>::type type;
};

注意为了方便起见,Ep在前面,Sp在后面,因为缺省参数必须在最后(没办法C++就是这么规定的)invoke可以很简单,就是把所有东西都forward过去调f:

template <class Fp, class... Args>
inline auto invoke(Fp&& f, Args&&... args)
-> decltype(std::forward<Fp>(f)(std::forward<Args>(args)...))
return std::forward<Fp>(f)(std::forward<Args>(args)...); }

当然,为了应付不同的callable,比如成员函数指针,或者有operator()的类,或者lambda什么的,我们可能还需要几个特化,不过这些都不重要,这里就不说了。

6、有了上面这些东西,之前的thread_data::run/run2就能运转了,它能通过make_tuple_indices和invoke解包tuple并调用f(args...)。

我们已经有了run,之所以需要再定义一个run2,是因为一条可能很多人猛一下想不起来的C++语法规定——member function template不能是虚函数。

Indices是一个template type,只能用一个template function接收,所以我们需要把run和run2拆开,run作为继承下来的虚函数做入口,run2接收Indices并用之前提到的方法调用f(args...)。

结论:C++写这种东西真的有点烦。

原文链接:https://www.zhihu.com/question/30553807/answer/48557177


推荐阅读

今年无论是应届生还是社招,行情都不容客观,C++低端岗位本来就少,加上市场形式,小方特地建立 『C++后端开发求职交流群』,小方也会不定期解答一些群友求职中遇到的问题,并不定期做一些求职技术相关的分享。


现在限时开放,有需要的同学尽快加群,需要加群交流的同学可以加微信 easy_coder,备注“加群”。


【声明】内容源于网络
0
0
CppGuide
专注于高质量高性能C++开发,站点:cppguide.cn
内容 1260
粉丝 0
CppGuide 专注于高质量高性能C++开发,站点:cppguide.cn
总阅读289
粉丝0
内容1.3k