std::thread 的构造-源码解析

std::thread 的构造-源码解析

我们这单章是为了专门解释一下 std::thread 是如何构造的,是如何创建线程传递参数的,让你彻底了解这个类。

我们以 MSVC 实现的 std::thread 代码进行讲解。

std::thread 的数据成员

  • 了解一个庞大的类,最简单的方式就是先看它的数据成员有什么

std::thread 只保有一个私有数据成员 _Thr

private:
    _Thrd_t _Thr;

_Thrd_t 是一个结构体,它保有两个数据成员:

using _Thrd_id_t = unsigned int;
struct _Thrd_t { // thread identifier for Win32
    void* _Hnd; // Win32 HANDLE
    _Thrd_id_t _Id;
};

结构很明确,这个结构体的 _Hnd 成员是指向线程的句柄,_Id 成员就是保有线程的 ID。

在64 位操作系统,因为内存对齐,指针 8 ,无符号 int 4,这个结构体 _Thrd_t 就是占据 16 个字节。也就是说 sizeof(std::thread) 的结果应该为 16

std::thread 的构造函数

std::thread 有四个构造函数,分别是:

  1. 默认构造函数,构造不关联线程的新 std::thread 对象。

    thread() noexcept : _Thr{} {}
    

    值初始化了数据成员 _Thr ,这里的效果相当于给其成员 _Hnd_Id 都进行零初始化

  2. 移动构造函数,转移线程的所有权,构造 other 关联的执行线程的 std::thread 对象。此调用后 other 不再表示执行线程失去了线程的所有权。

    thread(thread&& _Other) noexcept : _Thr(_STD exchange(_Other._Thr, {})) {}
    

    _STD 是一个宏,展开就是 ::std::,也就是 ::std::exchange,将 _Other._Thr 赋为 {} (也就是置空),返回 _Other._Thr 的旧值用以初始化当前对象的数据成员 _Thr

  3. 复制构造函数被定义为弃置的,std::thread 不可复制。两个 std::thread 不可表示一个线程,std::thread 对线程资源是独占所有权。

    thread(const thread&) = delete;
    
  4. 构造新的 std::thread 对象并将它与执行线程关联。表示新的执行线程开始执行

    template <class _Fn, class... _Args, enable_if_t<!is_same_v<_Remove_cvref_t<_Fn>, thread>, int> = 0>
        _NODISCARD_CTOR_THREAD explicit thread(_Fn&& _Fx, _Args&&... _Ax) {
            _Start(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...);
        }
    

前三个构造函数都没啥要特别聊的,非常简单,只有第四个构造函数较为复杂,且是我们本章重点,需要详细讲解。(注意 MSVC 使用标准库的内容很多时候不加 std::,脑补一下就行

如你所见,这个构造函数本身并没有做什么,它只是一个可变参数成员函数模板,增加了一些 SFINAE 进行约束我们传入的可调用对象的类型不能是 std::thread。函数体中调用了一个函数 _Start,将我们构造函数的参数全部完美转发,去调用它,这个函数才是我们的重点,如下:

template <class _Fn, class... _Args>
void _Start(_Fn&& _Fx, _Args&&... _Ax) {
    using _Tuple                 = tuple<decay_t<_Fn>, decay_t<_Args>...>;
    auto _Decay_copied           = _STD make_unique<_Tuple>(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...);
    constexpr auto _Invoker_proc = _Get_invoke<_Tuple>(make_index_sequence<1 + sizeof...(_Args)>{});

    _Thr._Hnd =
        reinterpret_cast<void*>(_CSTD _beginthreadex(nullptr, 0, _Invoker_proc, _Decay_copied.get(), 0, &_Thr._Id));

    if (_Thr._Hnd) { // ownership transferred to the thread
        (void) _Decay_copied.release();
    } else { // failed to start thread
        _Thr._Id = 0;
        _Throw_Cpp_error(_RESOURCE_UNAVAILABLE_TRY_AGAIN);
    }
}
  1. 它也是一个可变参数成员函数模板,接受一个可调用对象 _Fn 和一系列参数 _Args... ,这些东西用来创建一个线程。

  2. using _Tuple = tuple<decay_t<_Fn>, decay_t<_Args>...>

    • 定义了一个元组类型 _Tuple ,它包含了可调用对象和参数的类型,这里使用了 decay_t 来去除了类型的引用和 cv 限定。
  3. auto _Decay_copied = _STD make_unique<_Tuple>(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...)

    • 使用 make_unique 创建了一个独占指针,指向的是 _Tuple 类型的对象,存储了传入的函数对象和参数的副本。
  4. constexpr auto _Invoker_proc = _Get_invoke<_Tuple>(make_index_sequence<1 + sizeof...(_Args)>{})

    • 调用 _Get_invoke 函数,传入 _Tuple 类型和一个参数序列的索引序列(为了遍历形参包)。这个函数用于获取一个函数指针,指向了一个静态成员函数 _Invoke,用来实际执行线程。这两个函数都非常的简单,我们来看看:
     template <class _Tuple, size_t... _Indices>
     _NODISCARD static constexpr auto _Get_invoke(index_sequence<_Indices...>) noexcept {
         return &_Invoke<_Tuple, _Indices...>;
     }
     
     template <class _Tuple, size_t... _Indices>
     static unsigned int __stdcall _Invoke(void* _RawVals) noexcept /* terminates */ {
         // adapt invoke of user's callable object to _beginthreadex's thread procedure
         const unique_ptr<_Tuple> _FnVals(static_cast<_Tuple*>(_RawVals));
         _Tuple& _Tup = *_FnVals.get(); // avoid ADL, handle incomplete types
         _STD invoke(_STD move(_STD get<_Indices>(_Tup))...);
         _Cnd_do_broadcast_at_thread_exit(); // TRANSITION, ABI
         return 0;
     }
    

    _Get_invoke 函数很简单,就是接受一个元组类型,和形参包的索引,传递给 _Invoke 静态成员函数模板,实例化,获取它的函数指针。

    它的形参类型我们不再过多介绍,你只需要知道 index_sequence 这个东西可以用来接收一个由 make_index_sequence 创建的索引形参包,帮助我们进行遍历即可。

    _Invoke 是重中之重,它是线程实际执行的函数,如你所见它的形参类型是 void* ,这是必须的,要符合 _beginthreadex 执行函数的类型要求。虽然是 void*,但是我可以将它转换为 _Tuple* 类型,构造一个独占智能指针,然后调用 get() 成员函数获取底层指针,解引用指针,得到元组的引用初始化_Tup

    此时,我们就可以进行调用了,使用 std::invoke + std::move(默认移动) ,这里有一个形参包展开,_STD get<_Indices>(_Tup))...,_Tup 就是 std::tuple 的引用,我们使用 std::get<> 获取元组存储的数据,需要传入一个索引,这里就用到了 _Indices。展开之后,就等于 invoke 就接受了我们构造 std::thread 传入的可调用对象,调用可调用对象的参数,invoke 就可以执行了。

  5. _Thr._Hnd = reinterpret_cast<void*>(_CSTD _beginthreadex(nullptr, 0, _Invoker_proc, _Decay_copied.get(), 0, &_Thr._Id))

    • 调用 _beginthreadex 函数来启动一个线程,并将线程句柄存储到 _Thr._Hnd 中。传递给线程的参数为 _Invoker_proc(一个静态函数指针,就是我们前面讲的 _Invoke)和 _Decay_copied.get()(存储了函数对象和参数的副本的指针)。
  6. if (_Thr._Hnd) {

    • 如果线程句柄 _Thr._Hnd 不为空,则表示线程已成功启动,将独占指针的所有权转移给线程。
  7. (void) _Decay_copied.release()

    • 释放独占指针的所有权,因为已经将参数传递给了线程。
  8. } else { // failed to start thread

    • 如果线程启动失败,则进入这个分支
  9. _Thr._Id = 0;

    • 将线程ID设置为0。
  10. _Throw_Cpp_error(_RESOURCE_UNAVAILABLE_TRY_AGAIN);

    • 抛出一个 C++ 错误,表示资源不可用,请再次尝试。

总结

需要注意,libstdc++ 和 libc++ 可能不同,就比如它们 64位环境下 sizeof(std::thread) 的结果就是 8,它们的实现只保有一个线程 ID。

我们这里的源码解析涉及到的 C++ 技术很多,我们也没办法每一个都单独讲,那会显得文章很冗长,而且也不是重点。

相信你也感受到了,不会模板,你阅读标准库源码,是无稽之谈,市面上很多教程教学,教导一些实现容器,过度简化了,真要去出错了去看标准库的代码,那是不现实的。不需要模板的水平有多高,也不需要会什么元编程,但是基本的需求得能做到,得会,这里推荐一下:现代C++模板教程

  • 20
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值