百科问答小站 logo
百科问答小站 font logo



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

  

user avatar   windoze 网友的相关建议: 
      

绝大多数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...)。

结论:C++写这种东西神烦,要是有其它选择我立马叛逃(Rust目前还不够诱惑,等等再说)




  

相关话题

  为什么不同系统不能兼容同一个已编译的可执行二进制文件? 
  ARM 版处理器的 MacBook 推出后,你会选 X86 版还是 ARM 版? 
  为什么说C++很难? 
  如何理解计算物理中的元胞链接列表(Cell Linked List)算法? 
  如何看待 WebAssembly 这门技术? 
  程序员如何提高安全的编码能力? 
  这样一个数据库分析软件需难度高吗? 
  想做一个网站,如何跳过学代码编程,就直接把网站建? 
  宅总用的这是什么编辑器? 
  我在编程时给变量命名总觉得困难,有没有什么实用技巧? 

前一个讨论
英特尔和 Google 的 OKR 制度与我们一般所说的 KPI 有什么不同?
下一个讨论
亚洲人的平均身高在未来有没有可能超过欧洲?





© 2024-11-05 - tinynew.org. All Rights Reserved.
© 2024-11-05 - tinynew.org. 保留所有权利