详细讲解c++中线程类thread的实现,stl源码讲解之thread

Thread

本节我们来详细介绍一下c++中的线程类thread,在讲解的过程中会用到大量模板的知识,可以去看c++详解模板泛型编程,详解类模板的实现为什么不能放在cpp文件_泛型函数 cpo-CSDN博客

源码:

cpp 复制代码
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)...);
}

template <class _Tuple, size_t... _Indices>
static unsigned int __stdcall _Invoke(void* _RawVals) noexcept{
    const unique_ptr<_Tuple> _FnVals(static_cast<_Tuple*>(_RawVals));
    _Tuple& _Tup = *_FnVals.get();
    _STD invoke(_STD move(_STD get<_Indices>(_Tup))...);
    _Cnd_do_broadcast_at_thread_exit();
    return 0;
}

template <class _Tuple, size_t... _Indices>
_NODISCARD static constexpr auto _Get_invoke(index_sequence<_Indices...>) noexcept {
    return &_Invoke<_Tuple, _Indices...>;
}
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) {
        (void) _Decay_copied.release();
    } else {
        _Thr._Id = 0;
        _Throw_Cpp_error(_RESOURCE_UNAVAILABLE_TRY_AGAIN);
    }
}

enable_if_t<!is_same_v<_Remove_cvref_t<_Fn>, thread>, int> = 0是一个 SFINAE(Substitution Failure Is Not An Error)技巧,用于禁止使用thread对象作为可调用对象来构造新的thread对象。

我们从thread的构造函数开始,一步一步讲解线程的启动,构造函数会直接将接受的参数完美转发给_Start函数,这个函数会将传入的函数与参数进行包装,最后调用系统提供的线程函数来启动线程,此函数会将所有内容放入一个元组(_Tuple)中,具体类型是tuple<decay_t<_Fn>, decay_t<_Args>...>,这里的decay_t会对参数进行退化处理,它会移除引用、constvolatile 限定符,并且把数组类型转换为指针类型,函数类型转换为函数指针类型。之后使用make_unique构造了一个_Tuple对象,内部包含了退化后的可调用对象与所有参数。

这里需要注意_Get_invoke函数返回的是一个函数指针,它返回的不是_Invoke函数的返回值,而是_Invoke<_Tuple, _Indices...>这个函数的指针,返回的这个函数指针用于后续_beginthreadex函数的调用,调用_Get_invoke使用make_index_sequence在编译器生成了一个整数序列,这个生成的整数序列会被传递给 index_sequence<_Indices...>,并通过模板参数展开将索引保存在 _Indices 中,这里用于展开元组。

本教程使用的是Windows11所以标准库使用了Windows的线程api,_beginthreadex函数接受了我们的可调用对象与参数,参数传入的是元组这回在后续的_Invoke函数中展开,_beginthreadex 函数返回的是一个 uintptr_t 类型的值,它实际上是一个无符号整数类型,用于表示线程句柄。而 _Thr._Hnd 是一个 void* 类型的变量,用于存储线程句柄。因此,这就需要使用 reinterpret_cast<void*>_beginthreadex 返回的 uintptr_t 类型的值转换为 void* 类型,以便赋值给 _Thr._Hnd。传递&_Thr._Id的目的是获得线程id
reinterpret_cast 是 C++ 中的一种强制类型转换运算符,它主要用于将一种指针类型转换为另一种指针类型,甚至可以在不同类型的指针和整数类型之间进行转换。reinterpret_cast 提供了最底层、最直接的类型转换,它不进行任何类型检查或数据转换,只是简单地将二进制位从一种类型解释为另一种类型。

后续会检测线程创建成功,我们让_Decay_copied放弃其管理的资源,以免_Start运行完成后释放元组,如果不成功会将线程id置为0并抛出错误异常

最后我们来介绍一下调用函数_Invoke,它接受一个void*的参数(这就是我们的元组),这个参数是_beginthreadex传递给它的,首先通过static_cast将void*转换会元组类型,之后通过_STD get<_Indices>(_Tup) 借助索引 _Indices 从元组 _Tup 中获取元素。_Indices 是一个编译时生成的整数序列,通过参数包展开,能够依次获取元组中的所有元素,然后把这些元素传递给 std::invoke 调用的可调用对象。
_Cnd_do_broadcast_at_thread_exit 是一个内部函数,它的作用是在当前线程退出时广播一个条件变量。这通常用于线程同步,确保在某个线程退出时通知其他等待的线程。

invoke与get函数

std::invoke 是 C++17 引入标准库的一个通用调用工具,其用途是统一地调用可调用对象,像函数、函数指针、成员函数指针、成员对象指针、函数对象(重载了 operator() 的类实例)等,并且会把相应的参数传递给这些可调用对象。
std::invoke 会依据可调用对象的类型和参数的情况,采用合适的方式来调用可调用对象。例如,对于普通函数,它会直接调用;对于成员函数指针,它会正确处理对象实例的绑定。

std::get 是用于访问元组(std::tuple)、std::pair 或者 std::array 元素的函数模板,它能依据索引或者类型来获取容器中的元素。

_beginthreadex

函数原型如下:

cpp 复制代码
uintptr_t _beginthreadex(
    void* security,
    unsigned stack_size,
    unsigned (__stdcall* start_address)(void*),
    void* arglist,
    unsigned initflag,
    unsigned* thrdaddr
);
  1. security
    • 类型:void*
    • 含义:指向一个 SECURITY_ATTRIBUTES 结构体的指针,该结构体用于指定新线程的安全属性。如果传入 NULL,表示使用默认的安全属性。通常情况下,我们传入 NULL 即可。
  2. stack_size
    • 类型:unsigned
    • 含义:指定新线程的堆栈大小,以字节为单位。如果传入 0,则表示使用默认的堆栈大小。
  3. start_address
    • 类型:unsigned (__stdcall*)(void*)
    • 含义:指向新线程启动函数的指针。这个函数是新线程开始执行的入口点,它必须遵循 __stdcall 调用约定,返回类型为 unsigned,并且接受一个 void* 类型的参数。__stdcall 是一种调用约定,规定了参数传递的顺序(从右到左)和栈的清理方式(由被调用函数清理)。
  4. arglist
    • 类型:void*
    • 含义:传递给新线程启动函数的参数。这个参数会被作为 void* 类型传递给 start_address 所指向的函数。如果不需要传递参数,可以传入 NULL
  5. initflag
    • 类型:unsigned
    • 含义:指定新线程的初始状态。可以传入以下值:
      • 0:线程创建后立即开始执行。
      • CREATE_SUSPENDED:线程创建后处于挂起状态,需要调用 ResumeThread 函数来启动线程。
  6. thrdaddr
    • 类型:unsigned*
    • 含义:指向一个 unsigned 类型的变量,用于接收新线程的线程 ID。如果不需要获取线程 ID,可以传入 NULL
      如果线程创建成功,_beginthreadex 函数返回一个新线程的句柄,类型为 uintptr_t。这个句柄可以用于后续对线程的操作,如等待线程结束、终止线程等。如果线程创建失败,函数返回 0

_Thrd_t结构体

接下来我们再看看thread中的一个结构体_Thr,他是定义在下面:

cpp 复制代码
extern "C" {
using _Thrd_id_t = unsigned int;
struct _Thrd_t { // thread identifier for Win32
    void* _Hnd; // Win32 HANDLE
    _Thrd_id_t _Id;
};
}

_Thrd_t 结构体用于封装 Windows 线程的相关信息,包括线程句柄(_Hnd)和线程 ID(_Id)。通过这个结构体,可以方便地管理和操作线程,例如在创建线程时保存线程句柄和线程 ID,在后续的操作中使用这些信息来控制线程的行为。

这里将 _Thrd_t 结构体定义为 C 语言风格(使用 extern "C" 块)主要是为了实现与 C 语言代码的兼容性。_beginthreadex 是 Windows C 运行时库提供的函数,它遵循 C 语言的调用约定和命名规则。使用 extern "C" 可以确保 _Thrd_t 结构体在编译时生成的符号名遵循 C 语言的规则,从而可以与 C 语言代码无缝交互。

decay_t的主要作用

当创建线程时,参数需要被 安全地复制或移动 到线程的独立存储空间中,避免因原始参数生命周期结束(如局部变量被销毁)导致的悬空引用或指针问题。std::decay_t 的作用包括:移除引用 :将 T&T&& 退化为 T,强制生成值的副本(而非保留引用)。 处理数组和函数指针 :将数组类型 T[N] 退化为 T*,函数类型 F(Args...) 退化为 F(*)(Args...),使其可复制。

这使得 std::thread 构造函数能统一处理函数指针、函数对象或 lambda,确保线程内部操作的是独立的值,确保不会出现悬挂引用的问题。

相关推荐
猷咪9 分钟前
C++基础
开发语言·c++
IT·小灰灰11 分钟前
30行PHP,利用硅基流动API,网页客服瞬间上线
开发语言·人工智能·aigc·php
快点好好学习吧12 分钟前
phpize 依赖 php-config 获取 PHP 信息的庖丁解牛
android·开发语言·php
秦老师Q13 分钟前
php入门教程(超详细,一篇就够了!!!)
开发语言·mysql·php·db
烟锁池塘柳013 分钟前
解决Google Scholar “We‘re sorry... but your computer or network may be sending automated queries.”的问题
开发语言
是誰萆微了承諾13 分钟前
php 对接deepseek
android·开发语言·php
CSDN_RTKLIB16 分钟前
WideCharToMultiByte与T2A
c++
2601_9498683617 分钟前
Flutter for OpenHarmony 电子合同签署App实战 - 已签合同实现
java·开发语言·flutter
星火开发设计31 分钟前
类型别名 typedef:让复杂类型更简洁
开发语言·c++·学习·算法·函数·知识
蒹葭玉树42 分钟前
【C++上岸】C++常见面试题目--操作系统篇(第二十八期)
linux·c++·面试