C++ thread线程函数传参原理剖析
创始人
2025-05-30 13:59:31
0

知识的学习在于点滴记录,坚持不懈;知识的学习要有深度和广度,不能只流于表面,坐井观天;知识要善于总结,不仅能够理解,更知道如何表达!

目录

  • thread线程函数参数问题
  • 学习下thread类的源码
    • 从VS2019的C++类库中看thread的源码
    • invoke函数
    • 简化问题
    • std::ref(data)为什么就可以了

thread线程函数参数问题


从C++11开始,终于提供了语言级别的thread类库,从此可以通过C++语言编写多线程程序,做到一次编写,到处编译。thread对象可以传递普通函数、函数对象、lambda表达式等作为线程函数,使用起来非常方便。最近看到有人(tony)提出这样一个问题,代码片段:

#include 
#include void handler1(int b)  // 此处形参b,接收t1实参a的值,正确!!!
{std::cout << "do handler1" << std::endl;
}
void handler2(int &b)  // 此处形参b,想引用实参a,语法错误!!!为什么???
{std::cout << "do handler2" << std::endl;
}
int main()
{int a = 10;std::thread t1(handler1, a);t1.join();// std::thread t2(handler2, a);   这里编译错误!!!// t2.join();
}

tony定义了一个thread对象,绑定了handler2线程函数,handler2的参数是一个普通的左值引用变量int &b,为什么不能接收实参a? 也就是std::thread t2(handler2, a); 这句代码直接编译错误,在visual studio 2019上(编译默认使用C++ 14标准)错误信息如下:
在这里插入图片描述
可以看到vs报错“invoke未找到匹配的重载函数”,具体原理后面给大家详细解释。如果线程函数非得用int &b这样的左值引用来接收,使用的时候如下即可:

#include 
#include void handler1(int b)
{std::cout << "do handler1" << std::endl;
}
void handler2(int &b)
{std::cout << "do handler2" << std::endl;
}
int main()
{int a = 10;std::thread t1(handler1, a);t1.join();std::thread t2(handler2, std::ref(a));  //  注意这里使用std::ref(a)即可!!!t2.join();
}

tony的问题就是,他写的线程函数handler2,形参是int &b,为什么不能直接接收实参变量a,但是用std::ref(a)又可以了,底层原理是什么?

学习下thread类的源码

要从原理上解释上面的问题,我们需要看看thread类的源码,主要看从定义一个thread对象,到线程函数的调用,中间都执行了哪些代码操作,通过查看源码,看看造成上面问题的根本原因是什么?

从VS2019的C++类库中看thread的源码

拷贝thread的源码,我们实现一个自己的线程类Slthread,如下:

#include 
#include 
using namespace std;class Slthread
{
public:template static unsigned int __stdcall _Invoke(void* _RawVals) noexcept /* terminates */ {// adapt invoke of user's callable object to _beginthreadex's thread procedureconst unique_ptr<_Tuple> _FnVals(static_cast<_Tuple*>(_RawVals));_Tuple& _Tup = *_FnVals;_STD invoke(_STD move(_STD get<_Indices>(_Tup))...);return 0;}template _NODISCARD static constexpr auto _Get_invoke(index_sequence<_Indices...>) noexcept {return &_Invoke<_Tuple, _Indices...>;}template void _Start(_Fn&& _Fx, _Args&&... _Ax) {using _Tuple = tuple, 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)>{});_Invoker_proc(_Decay_copied.get());_Decay_copied.release();}template explicit Slthread(_Fn&& _Fx, _Args&&... _Ax) {_Start(std::forward<_Fn>(_Fx), std::forward<_Args>(_Ax)...);}
};void handler(int b)
{std::cout << "do handler." << std::endl;
}
int main()
{int a= 10;Slthread t(handler, a);
}

我们拷贝了thread的部分代码,输出了一个自己的Slthread类,运行上面代码结果

do handler.

看到handler线程函数正常执行了(我们实际没有调用线程函数,handler还是在主线程中执行的,本文主要讨论线程函数参数传递的原理,handler是否真的在另一个线程中运行,不是当前的重点),如果把handler的原型修改成void handler(int &b),编译依然是最上面提到的错误:

void handler(int &b)   //  注意看这里的形参,从int b===>  int &b
{std::cout << "do handler." << std::endl;
}
int main()
{int a = 10;Slthread t(handler, a);
}

既然是我们自己输出的Slthread类代码,我们就能知道最终报错的代码行是:
在这里插入图片描述

invoke函数

跟踪Slthread的源码,看到函数调用关系,Slthread构造函数 =》_Start函数 =》 _Get_invoke函数 =》invoke函数,这里涉及的C++知识点比较多,我们直接来到invoke函数,看到调用它时传入的实参是:_STD move(_STD get<_Indices>(_Tup))… 这是什么呢,帮助大家翻译一下:
1、std::move是移动语义的意思,把参数转换成右值进行传递
2、_Tup是一个Tuple(C++的tuple类型,一个存储异构数据的集合)类型,里面放了线程函数类型参数类型,然后通过std::get<0>(_Tup), std::get<1>(_Tup) 获取到tuple集合里面存储的线程函数和参数,传递给invoke进行调用。

比如代码如果是:

void handler(int b)
{std::cout << "do handler." << std::endl;
}
int main()
{int a = 10;Slthread t(handler, a);
}

那么invoke的调用就可以翻译成:
std::invoke(std::move(handler), std::move(a)),此时handler的类型是void (*)(int),a的类型是int。

代码如果是:

void handler(int &b)
{std::cout << "do handler." << std::endl;
}
int main()
{int a = 10;Slthread t(handler, a);
}

那么invoke的调用就可以翻译成:
std::invoke(std::move(handler), std::move(a)),此时handler的类型是void (*)(int&),a的类型是int。
然后我们找到invoke函数的定义,缩减如下:

template 
auto invoke(_Callable&& _Obj, _Ty1&& _Arg1, _Types2&&... _Args2) noexcept 
{return static_cast<_Callable&&>(_Obj)(static_cast<_Ty1&&>(_Arg1), static_cast<_Types2&&>(_Args2)...);
}

我们看到,实际上invoke函数的实现,就是调用第一个参数_Obj这个函数(handler函数),然后把后面的当作参数(实参a)传递给_Obj函数开始执行,所以啰嗦了半天,文章开始的问题,实际上就和下面的代码问题是等价的:

简化问题

#include 
using namespace std;template 
auto myinvoke(_Callable&& _Obj, _Ty1&& _Arg1, _Types2&&... _Args2) noexcept 
{return static_cast<_Callable&&>(_Obj)(static_cast<_Ty1&&>(_Arg1), static_cast<_Types2&&>(_Args2)...);
}
void handler1(int b)
{std::cout << "do handler." << std::endl;
}
void handler2(int &b)
{std::cout << "do handler." << std::endl;
}
int main()
{int a= 10;myinvoke(std::move(handler1), std::move(a));  // 编译正确,能够正常调用handler1函数// myinvoke(std::move(handler2), std::move(a)); // 编译出错!!!
}

现在一眼就可以看出来,为什么线程函数不能使用普通的左值引用int &b来接收实参a了,因为实参a传递的是右值,不能被一个左值引用变量int &b接收啊,类似:

int &b= std::move(a) // std::move(a)在上面invoke函数中,对应的就是static_cast<_Ty1&&>(_Arg1)

很明显,上面的转换肯定是不行的。当然现在既然知道原理了,线程函数实参传递的是右值,那么形参用右值引用,或者常引用都可以,如下:

// void handler2(int &data)     // 这个是错误的
void handler2(int &&data)       // OK的
void handler2(const int &data)  // OK的

std::ref(data)为什么就可以了

下面代码为什么就可以了?

#include 
using namespace std;template 
auto myinvoke(_Callable&& _Obj, _Ty1&& _Arg1, _Types2&&... _Args2) noexcept 
{return static_cast<_Callable&&>(_Obj)(static_cast<_Ty1&&>(_Arg1), static_cast<_Types2&&>(_Args2)...);
}void handler2(int &data)
{std::cout << "do handler." << std::endl;
}
int main()
{int data = 10;myinvoke(std::move(handler2), std::move(std::ref(data)));
}

考虑std::ref(data)做了哪些? 翻看std::ref的源码,它返回一个包装类对象,类型是reference_wrapper,成员变量是一个指针,指向了data,如下图解释:
在这里插入图片描述
也就是实参传递std::ref(data),实际上最终转换成 static_cast(std::move(std::ref(data))) => int &data ,那到底一个reference_wrapper是怎么强转成int&类型的??? 还记得我们讲的C++类和其它类型的转换是怎么进行的吗?
1、其它类型 => 转换成类类型,主要看类有没有合适的构造函数,显示或者隐式生成临时对象
2、类类型 => 转换成其它类型,主要看类有没有提供类型重载函数 operator 类型() 这样的函数
看一下reference_wrapper的源码,你可以马上发现这个类型重载函数:
在这里插入图片描述
也就是reference_wrapper对象,会自动调用上面的operator _Ty&(),即就是operator int&()类型重载函数,返回一个左值变量*_Ptr(就是实参data本身),这样就可以用一个普通的左值引用变量(线程函数的形参int &data),来引用这个*_Ptr了。

相关内容

热门资讯

喜欢穿一身黑的男生性格(喜欢穿... 今天百科达人给各位分享喜欢穿一身黑的男生性格的知识,其中也会对喜欢穿一身黑衣服的男人人好相处吗进行解...
发春是什么意思(思春和发春是什... 本篇文章极速百科给大家谈谈发春是什么意思,以及思春和发春是什么意思对应的知识点,希望对各位有所帮助,...
网络用语zl是什么意思(zl是... 今天给各位分享网络用语zl是什么意思的知识,其中也会对zl是啥意思是什么网络用语进行解释,如果能碰巧...
为什么酷狗音乐自己唱的歌不能下... 本篇文章极速百科小编给大家谈谈为什么酷狗音乐自己唱的歌不能下载到本地?,以及为什么酷狗下载的歌曲不是...
华为下载未安装的文件去哪找(华... 今天百科达人给各位分享华为下载未安装的文件去哪找的知识,其中也会对华为下载未安装的文件去哪找到进行解...
家里可以做假山养金鱼吗(假山能... 今天百科达人给各位分享家里可以做假山养金鱼吗的知识,其中也会对假山能放鱼缸里吗进行解释,如果能碰巧解...
四分五裂是什么生肖什么动物(四... 本篇文章极速百科小编给大家谈谈四分五裂是什么生肖什么动物,以及四分五裂打一生肖是什么对应的知识点,希...
怎么往应用助手里添加应用(应用... 今天百科达人给各位分享怎么往应用助手里添加应用的知识,其中也会对应用助手怎么添加微信进行解释,如果能...
一帆风顺二龙腾飞三阳开泰祝福语... 本篇文章极速百科给大家谈谈一帆风顺二龙腾飞三阳开泰祝福语,以及一帆风顺二龙腾飞三阳开泰祝福语结婚对应...
美团联名卡审核成功待激活(美团... 今天百科达人给各位分享美团联名卡审核成功待激活的知识,其中也会对美团联名卡审核未通过进行解释,如果能...