使用类成员函数初始化std::thread时候遇到的坑
2018-05-01
问题
这两天在写代码的过程中连续遇到两次,在使用类成员函数初始化std::thread时候报错,都是重复释放的错误,查了好久才查到原因是在初始化传入类实例的时候会把传入的实例销毁,而我在类的析构函数中释放了相关资源,销毁时调用了析构函数导致资源被提前释放造成了这样的bug。
原理
在 这里 详细的解答了造成类对象提前释放的原因和解决的方法。
示例代码
#include <iostream>
#include <thread>
class MyClass {
public:
MyClass(){
std::cout << "MyClass constructor is called" << std::endl;
}
~MyClass(){
std::cout << "MyClass destructor is called" << std::endl;
}
void start(){
std::cout << "MyClass is starting" << std::endl;
}
};
int main()
{
MyClass mine;
std::thread t(&MyClass::start, mine);
t.join();
}
在初始化thread的时候,不仅传入给函数的对象是按值传递拷贝到新线程空间的,需要传的类对象本身也是默认按值传递拷贝到新线程空间,这样就导致了类对象mine被拷贝到std::thread构造函数的参数之中,构造函数的参数在构造函数执行完之后自然需要被释放,就导致了所属类的析构函数被意外调用。
解决
对于这个问题解决的方法也很简单,把类对象的传递加上std::ref,使之作为引用传递到std::thread中去,当引用的生命周期到了的时候就不会对原有的类对象有任何影响了。
新的问题
这样,困扰报错的问题就被解决了,不过因此就有了新的问题:为什么初始化std::thread需要默认将初始化参数进行按值传递。刚开始学习std::thread的时候就对传参是默认的行为是按值传递,如果要按引用传递要加std::ref做引用包装感到好奇, 这里 对这样的默认按值传递行为作出了解释:简单来说,由于std::thread所执行的线程代码和原有线程是分离的,如果传参是默认按引用传递的话,需要保证std::thread产生的线程运行的时候,在原有线程中传入的变量不能被释放,否则就会产生错误。因此C++规范将默认行为定义为按值传递,当变量拷贝了之后,原线程中的变量无论是否被释放都不会产生影响,当需要按引用传递的时候加上引用包装std::ref即可。
gcc实现
最后我们来看一下gcc源码是怎么实现对传参进行默认按值传递的。
我电脑上是gcc 7.3.1,fedora 26 server,打开/usr/include/c++/7/thread
可以看到和std::thread相关的头文件内容,std基本都是模板类,在头文件中可以看到大部分的实现。
转到构造函数看看:
template<typename _Callable, typename... _Args>
explicit
thread(_Callable&& __f, _Args&&... __args)
{
#ifdef GTHR_ACTIVE_PROXY
// Create a reference to pthread_create, not just the gthr weak symbol.
auto __depend = reinterpret_cast<void(*)()>(&pthread_create);
#else
auto __depend = nullptr;
#endif
_M_start_thread(_S_make_state(
__make_invoker(std::forward<_Callable>(__f),
std::forward<_Args>(__args)...)),
__depend);
}
这里比较明显依次调用了__make_invoker
,_S_make_state
,_M_start_thread
,我们首先来看__make_invoker
:
// Returns a call wrapper that does
// INVOKE(DECAY_COPY(__callable), DECAY_COPY(__args)).
template<typename _Callable, typename... _Args>
static __invoker_type<_Callable, _Args...>
__make_invoker(_Callable&& __callable, _Args&&... __args)
{
return { {
std::make_tuple(std::forward<_Callable>(__callable),
std::forward<_Args>(__args)...)
} };
}
注释里写的很明白,应该就是这里把默认按引用传参退化成了按值传参,看下std::make_tuple
的源码,在/usr/include/c++/7/tuple
中:
template<typename... _Elements>
constexpr tuple<typename __decay_and_strip<_Elements>::__type...>
make_tuple(_Elements&&... __args)
{
typedef tuple<typename __decay_and_strip<_Elements>::__type...> __result_type;
return __result_type(std::forward<_Elements>(__args)...);
}
这里的__result_type
对应了__decay_and_strip<T>::__type
,代码在/usr/include/c++/7/type_traits
:
template<typename _Tp>
struct __decay_and_strip
{
typedef typename __strip_reference_wrapper<
typename decay<_Tp>::type>::__type __type;
};
可以看出先用了decay
对参数进行退化处理,再用__strip_reference_wrapper
进行引用脱包,看下std::decay
具体的处理方式,也在/usr/include/c++/7/type_traits
:
template<typename _Tp>
class decay
{
typedef typename remove_reference<_Tp>::type __remove_type;
public:
typedef typename __decay_selector<__remove_type>::__type type;
};
其中__remove_type
和__decay_selector
分别是:
/// remove_reference
template<typename _Tp>
struct remove_reference
{ typedef _Tp type; };
template<typename _Tp>
struct remove_reference<_Tp&>
{ typedef _Tp type; };
template<typename _Tp>
struct remove_reference<_Tp&&>
{ typedef _Tp type; };
// Decay trait for arrays and functions, used for perfect forwarding
// in make_pair, make_tuple, etc.
template<typename _Up,
bool _IsArray = is_array<_Up>::value,
bool _IsFunction = is_function<_Up>::value>
struct __decay_selector;
// NB: DR 705.
template<typename _Up>
struct __decay_selector<_Up, false, false>
{ typedef typename remove_cv<_Up>::type __type; };
template<typename _Up>
struct __decay_selector<_Up, true, false>
{ typedef typename remove_extent<_Up>::type* __type; };
template<typename _Up>
struct __decay_selector<_Up, false, true>
{ typedef typename add_pointer<_Up>::type __type; };
首先__remove_type
将传入的类型强制去除其引用状态,然后使用__decay_selector
判断对类型是数组类型或函数类型或是普通类型,我们所要看的普通类型处理是remove_cv
,即去除类型的const状态。到这里std::decay
就已经完成了,对于我们传入的普通类型参数,将其去除了引用和const状态,形成了默认按传值传参的处理方式。
接下来__strip_reference_wrapper
的则是处理了std::ref生成的引用包装,让按引用传参可以正常生效,看下__strip_reference_wrapper
的源码:
// Helper which adds a reference to a type when given a reference_wrapper
template<typename _Tp>
struct __strip_reference_wrapper
{
typedef _Tp __type;
};
template<typename _Tp>
struct __strip_reference_wrapper<reference_wrapper<_Tp> >
{
typedef _Tp& __type;
};
很简单,当传入的类型是普通类型的时候,不做任何处理,当传入的类型是std::ref生成的reference_wrapper
,就将其脱去引用包装,转化为普通的引用类型。
到这里__decay_and_strip
的操作就全部完成了,这样就可以产生默认按值传参,和用std::ref指定按引用传参的效果。
所有的疑问终于都解决了。