介绍thread源码,运行机制,看完可以知道参数在线程中的传递过程,为什么不让拷贝构造,为什么左值引用会编译错误,想利用左值引用修改参数该怎么办,指针参数传递的过程.

先看源码

thread构造函数内部通过forward原样转换传递给_Start函数。

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

**template <class _Fn, class… _Args, …>**定义模板参数:

_Fn:可调用对象(函数、lambda、仿函数等)的类型。

_Args…:可调用对象的参数类型包(变长模板参数)。

enable_if_t<!is_same_v<_Remove_cvref_t<_Fn>, thread>, int> = 0

这是一个 SFINAE(替换失败不是错误)约束,确保 _Fn 不是 std::thread 类型

_Remove_cvref_t<_Fn>:移除 _Fn 的 const、volatile 和引用修饰符。

!is_same_v<…, thread>:检查 _Fn 是否与 std::thread 类型相同,若相同则禁用此重载。

enable_if_t<…, int>:若条件为真,则模板有效;否则忽略此重载。

= 0:默认模板参数,无实际作用,仅为语法需要。

_NODISCARD_CTOR: 编译器扩展宏,表示构造函数禁止被忽略(类似 [[nodiscard]]),避免未使用的线程对象。

explicit: 禁止隐式转换构造函数,必须显式调用。

thread(_Fn&& _Fx, _Args&&… _Ax)

参数列表:

_Fx:可调用对象的万能引用(_Fn**&& 是转发引用**)。

_Ax…:可调用对象参数的万能引用包。

为什么千方百计不让传入的函数类型与thread相同

即不允许拷贝构造

  1. 线程对象的唯一性:

每个 std::thread 对象管理一个唯一的线程执行资源(如操作系统线程句柄)。如果允许拷贝,会导致多个 std::thread 对象管理同一个线程资源,引发资源竞争或重复释放。

  1. 生命周期问题:

线程的执行可能比其管理的 std::thread 对象存活更久,拷贝会导致难以追踪线程状态。

替代方案:移动构造

1
2
3
4
5
如果需要传递线程所有权,使用 std::move:
std::thread t2([]() { std::cout << "Hello"; });
std::thread t1 = std::move(t2); // 正确:移动构造
t1.join(); // t1 现在管理原 t2 的线程
移动后:t2 变为空状态(不再关联任何线程),t1 接管原 t2 的资源

核心流程

  1. _Start 函数:将可调用对象 _Fx 和参数 _Ax… 打包为元组 _Tuple,存储在堆上(unique_ptr)。

_Start 函数内部就是启动了一个线程_beginthreadex执行回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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)>{});
#pragma warning(push)
#pragma warning(disable : 5039) // pointer or reference to potentially throwing function passed to
// extern C function under -EHc. Undefined behavior may occur
// if this function throws an exception. (/Wall)
_Thr._Hnd = reinterpret_cast<void*>(
_CSTD _beginthreadex(
nullptr, // 安全属性(默认)
0, // 栈大小(默认)
_Invoker_proc, // 线程入口函数
_Decay_copied.get(), // 参数(元组指针)
0, // 初始状态(0=立即运行)
&_Thr._Id) // 线程ID输出
);
#pragma warning(pop)
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); // 抛出异常
}
}

decay_t<_Fn>:移除 _Fx 的引用和 const/volatile 修饰符,确保存储原始类型。

**如果原始参数是 左值引用(如 int&),decay_t 会将其退化为 int(值类型)。

如果原始参数是 右值引用(如 int&&),decay_t 仍会退化为 int(值类型)**

tuple<…>:将可调用对象和参数打包为一个元组,便于统一管理。

make_unique<_Tuple>:在堆上创建元组的唯一所有权指针(unique_ptr),避免拷贝问题。

forward:完美转发参数,保留左值/右值语义。

  1. _Get_invoke:生成线程入口函数 _Invoke 的指针,该函数负责从元组中解包并调用 _Fx(_Ax…)。

_Get_invoke:生成一个静态函数指针,作为线程入口。内部逻辑:从元组中解包 _Fx 和 _Ax…,调用 _Fx(_Ax…)。

make_index_sequence:生成编译期整数序列(如 0,1,2,…),用于元组解包。

_beginthreadex:Windows API,创建线程。将 _Invoker_proc 作为入口,_Decay_copied.get() 作为参数传递。

reinterpret_cast<void*>:将线程句柄转换为 void* 存储。

成功时:_Decay_copied.release() 释放 unique_ptr 的所有权,线程内部负责释放元组内存。

失败时:抛出 std::system_error,错误码为 resource_unavailable_try_again。

  1. _Invoke 函数:实际执行 std::invoke,调用用户提供的可调用对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
_NODISCARD static constexpr auto _Get_invoke(index_sequence<_Indices...>) noexcept {
return &_Invoke<_Tuple, _Indices...>;
}//_Get_invoke里面
##################################################################
static unsigned int __stdcall _Invoke(void* _RawVals) {
const unique_ptr<_Tuple> _FnVals(static_cast<_Tuple*>(_RawVals));
_Tuple& _Tup = *_FnVals;
_STD invoke(_STD move(_STD get<_Indices>(_Tup))...); // 关键行
//move(get<_Indices>(_Tup)):
从元组中提取参数,并强制转为右值(move 的作用)。
这意味着:所有参数都以右值形式传递给 invoke。
}
##################################################################
invoke实际就是调用了_Call函数,_Call的作用就是调用回调函数,并传递给回调函数参数
CONSTEXPR17 auto invoke(_Callable&& _Obj, _Ty1&& _Arg1, _Types2&&... _Args2) noexcept(
noexcept(_Invoker1<_Callable, _Ty1>::_Call(
static_cast<_Callable&&>(_Obj),
static_cast<_Ty1&&>(_Arg1),
static_cast<_Types2&&>(_Args2)...)
)
)

可以理解为调用_Get_invoke就是调用invoke(_STD move(_STD get<_Indices>(_Tup))...);
invoke(_STD move(_STD get<_Indices>(_Tup))...);就是将回调函数和参数传递给invoke

从元组中提取参数,并强制转为右值(move 的作用)。这意味着:所有参数都以右值形式传递给 invoke

std::thread 的设计假设:

它认为所有参数都应被移动或拷贝到新线程,因此强制以右值形式传递(避免隐式共享主线程的数据)。

用户错误假设:

用户可能误以为参数会按原始类型(如左值引用)传递,但实际上 std::thread 会主动切断与原数据的联系。

原理总结

参数打包到元组 decay_t移除引用,forward保留语义 左值引用会退化为值类型
线程启动 元组被移动到新线程 主线程的局部变量可能失效
_Invoke 调用 参数以右值形式传递(move 左值引用函数需改为值或显式用 std::ref
std::invoke 调用用户函数,参数为右值 检查函数签名是否匹配
  1. 参数打包阶段:退化为值类型

传入参数,在打包成元组时被保存为了非引用类型,在传入内部invoke函数时又被强转成了右值类型.所以对于用户外面给定的函数

1
2
3
4
5
6
void foo(int& x) {}

int main() {
int val = 42;
std::thread t(foo, val); // val是int&但decay_t<int&>结果为int.元组中存储的是 int 的副本。
}
  1. 参数取出阶段:强制转为右值

将元组中的每个参数强制转为右值引用(T&&),无论其原始类型是什么。

1
2
// 假设元组 _Tup 存储了 int 类型(值为 42)
invoke(foo, move(get<1>(_Tup))); // 实际调用:foo(static_cast<int&&>(42))

即使 foo 本应传入的参数是 int&,这里也会尝试传递 int&& → 编译错误(左值引用不能绑定右值)。

为什么这么设计?

  1. 线程安全性

避免悬空引用:如果允许直接传递左值引用,主线程的局部变量可能在线程运行时被销毁(如 detach 场景)。

明确所有权转移:强制右值传递表明参数的所有权已转移到新线程,原线程不应再访问。

  1. 性能优化

避免不必要的拷贝:若参数本身是右值(如临时对象),move 可以高效转移资源(如 std::string 的内部缓冲区)。

  1. 统一处理

无论用户传递的是左值还是右值,最终都以右值形式传递 → 简化内部实现。

可是就是需要修改左值参数怎么办?

使用 std::ref 显式包装引用:将 val 包装为 reference_wrapper,decay_t 会保留其引用特性。

1
2
3
4
5
6
7
8
void foo(int& x) {}
int main() {
int val = 42;
std::thread t(foo, std::ref(val)); // 绕过 decay_t,保留引用语义
t.join();
}
如果不需要修改直接值传递就好了,参数会被拷贝到线程
void foo(int x) {} // 改为值传递

那为什么使用std::ref就可以实现引用效果呢?

看下std::ref的源码

1
2
3
4
5
6
7
8
9
10
template <class _Ty>
_NODISCARD _CONSTEXPR20 reference_wrapper<_Ty> ref(_Ty& _Val) noexcept {
return reference_wrapper<_Ty>(_Val);
}
reference_wrapper是一个类类型,说白了就是将参数的地址和类型保存起来。

_CONSTEXPR20 reference_wrapper(_Uty&& _Val) noexcept(noexcept(_Refwrap_ctor_fun<_Ty>(_STD declval<_Uty>()))) {
_Ty& _Ref = static_cast<_Uty&&>(_Val);
_Ptr = _STD addressof(_Ref);
}

当我们要使用这个类对象时,自动转化为取内部参数的地址里的数据即可,就达到了和实参关联的效果

1
2
3
4
5
6
 _CONSTEXPR20 operator _Ty&() const noexcept {
return *_Ptr;
}
_NODISCARD _CONSTEXPR20 _Ty& get() const noexcept {
return *_Ptr;
}

所以我们可以这么理解传递给thread对象构造函数的参数,仍然作为右值被保存,如ref(int)实际是作为reference_wrapper(int)对象保存在threa的类成员里。

而调用的时候触发了仿函数()进而获取到外部实参的地址内的数据。

或者传递指针或智能指针,但需要确保val生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void foo(int* x) {}

int main() {
int val = 42;
std::thread t(foo, &val); // 需确保 val 的生命周期
t.join();
}
decay_t<_Fn>:移除 _Fx 的引用和 const/volatile 修饰符,确保存储原始类型。
decay_t<int*> 仍为 int*(指针不受 decay_t 影响)。
元组中存储的是指针的值(即地址的副本)。

_STD invoke(_STD move(_STD get<1>(_Tup))...);
// 实际调用:foo(static_cast<int*&&>(ptr_val))
//虽然用了 move,但指针本身是标量类型,移动等同于拷贝(地址值不变)。
//最终调用用户函数:
//传递的是指针的右值引用(int*&&),但函数参数是 int*,兼容右值→左值转换。

指针传递风险:悬空指针问题 子线程可能访问已释放的内存或重复释放

1
2
3
4
5
6
7
8
9
10
void danger() {
int* ptr = new int(42);
std::thread t([](int* p) {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << *p; // 可能访问已释放的内存
delete p; // 重复释放风险
}, ptr);
t.detach();
delete ptr; // 主线程立即释放内存,导致子线程可能访问已释放的内存...
}

解决方案:独占指针转移所有权 或者 共享指针共享所有权

1
2
3
4
5
6
7
8
9
std::thread t([](std::unique_ptr<int> p) {
std::cout << *p;
}, std::make_unique<int>(42)); // 明确所有权转移
##########################################################

auto ptr = std::make_shared<int>(42);
std::thread t([](std::shared_ptr<int> p) {
std::cout << *p;
}, ptr); // 引用计数保证生命周期