- 论坛徽章:
- 44
|
哈哈,忽然想起我以前在逼乎上贴过的一个答案,讲C++11的thread包装是怎么做的。
绝大多数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<Indices>()...);
复制代码
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...)。 |
|