引言
最近在看《C++并发编程实战》的时,书上有一句话这么写:
这些参数会拷贝至新线程的内存空间中(同临时变量一样)。即使函数中的参数是引用的形式,拷贝操作也会执行。
这有悖linux c的Pthreads API和引用的含义,我便想试试,如果是引用,子线程究竟是和父进程共享一个对象,还是说各持一份拷贝。
由于cout并不具有线程安全性(来自陈硕),且使用多次输出运算符相当于调用多次operator<<(),可能使输出变得杂乱,我并没有使用它。方便起见,我也没有使用condition这样的同步原语。
于是我的测试代码是这么写的:
#include <bits/stdc++.h>
#include <unistd.h>
using namespace std;
void thread_func(string& data) {
printf("thread : initial data is %s\n", data.c_str());
data.assign("child thread's data\n");
printf("thread : data altered\n");
printf("thread : final data is %s\n", data.c_str());
}
int main(int argc, char *argv[]) {
const char* base = "main thread's data";
string data(base);
printf("main : initial data is %s\n", data.c_str());
thread t(thread_func, data);
// thread t(thread_func, ref(data));
sleep(5);
data.compare(base) ?
cout << "book is wrong" << endl :
cout << "book is correct" << endl;
t.join();
return 0;
}
这段代码并不能通过编译,完整报错是:
In template: static_assert failed due to requirement ‘__is_invocable<void (*)(int &), int>::value’ “std::thread arguments must be invocable after conversion to rvalues”。
我求助了百度翻译,但它并不靠谱:
在模板中:由于要求“\u is \u invocable<void(*)(int&),int>::value’”std::thread arguments must be invocable after conversion to rvalues”,static \u assert失败
大致意思是:
在模板中:由于requirement ‘__is_invocable<void (*)(int &), int>::value’ ,static_assert失败,在转化为右值后std::thread参数必须被调用。
std::thread部分源码长这样:
thread() noexcept = default;
template<typename _Callable, typename... _Args,
typename = _Require<__not_same<_Callable>>>
explicit
thread(_Callable&& __f, _Args&&... __args)
{
static_assert( __is_invocable<typename decay<_Callable>::type,
typename decay<_Args>::type...>::value,
"std::thread arguments must be invocable after conversion to rvalues"
);
#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
// A call wrapper holding tuple{DECAY_COPY(__f), DECAY_COPY(__args)...}
using _Invoker_type = _Invoker<__decayed_tuple<_Callable, _Args...>>;
_M_start_thread(_S_make_state<_Invoker_type>(
std::forward<_Callable>(__f), std::forward<_Args>(__args)...),
__depend);
}
一些我知道的细节:
- 对于构造函数,thread提供了两个版本,一个是默认的,另一个是模板,而具有自动推导参数的能力,因此std::thread允许我们使用各种的callable entity去设置线程的回调函数。此外,可变模板参数使我们能方便的控制回调函数参数的类型。
注意这里不能使用function,毕竟function是要指定可调用对象的签名的,而模板能帮助我们推导出来,适应性更强。 - __is_invocable是一个模板,作用应该是检测传入的参数是被调用。当不成立使,value为false,触发静态断言,停止编译,我并没有去深究这个工具模板的运作方式。
- &&能够保持模板形参的const和左右值属性,在此处会发生一个引用折叠。
- 用std::forward进行转发,std::forward返回T&&,如果是左值将被折叠为T&,仍然是一个左值,如果是右值将被折叠为T&&,仍然是一个右值。
解决方案
书上也提供了一种解决方案(怪我没有往下看…):使用std::ref。
原因是:内部代码会将拷贝的参数以右值的方式进行传递,这是为了那些只支持移动的类型。
关于这点我又觉得很好奇,去找了源码,但只是大概,并没有本质的解决这个问题。
std::ref的源码:
/// Denotes a reference should be taken to a variable.
template<typename _Tp>
_GLIBCXX20_CONSTEXPR
inline reference_wrapper<_Tp>
ref(_Tp& __t) noexcept
{
return reference_wrapper<_Tp>(__t); }
可以看到std::ref实际上是一个函数模板,它本身并不能成为引用,而是通过模板引用的第一条特殊推导规则,_Tp被推导为_Tp&,再交给reference_wrapper。
再看看reference_wrapper的源码:
template<typename _Tp>
class reference_wrapper
#if __cplusplus <= 201703L
// In C++20 std::reference_wrapper<T> allows T to be incomplete,
// so checking for nested types could result in ODR violations.
: public _Reference_wrapper_base_memfun<typename remove_cv<_Tp>::type>
#endif
{
_Tp* _M_data;
由于其他地方我也看不懂,就不放上来了,哈哈哈哈哈。
大致看出来,reference_wrapper用指针来实现引用,实际上,大多数编译器也是这么干的。
因此,std::ref使用于那些只能使用值传递而不能使用引用传递的地方。
注意看到源码中还有一个std::decay,关于这个我又去找了cplusplus官网的解释。
如下:
If T is a function type, a function-to-pointer conversion is applied and the decay type is the same as: add_pointer<T>::type
If T is an array type, an array-to-pointer conversion is applied and the decay type is the same as: add_pointer<remove_extent<remove_reference<T>::type>::type>::type
Otherwise, a regular lvalue-to-rvalue conversion is applied and the decay type is the same as: remove_cv<remove_reference<T>::type>::type.
看最下面一行,对于其他类型,会应用一个从左值到右值的转换,可能说这里就是那个丢失引用性质的地方。
再看源码:
#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
// A call wrapper holding tuple{DECAY_COPY(__f), DECAY_COPY(__args)...}
using _Invoker_type = _Invoker<__decayed_tuple<_Callable, _Args...>>;
_M_start_thread(_S_make_state<_Invoker_type>(
std::forward<_Callable>(__f), std::forward<_Args>(__args)...),
__depend);
}
可以看到这里实际上最终还是调用到pthread_create,此外还用到了一个tuple。
我们知道pthread_create参数类型是void*,我想应该是C++为了实现可变模板参数,把…中的内容放到一个tuple中,而为了照顾那些只支持右值传递的类型,tuple中放的是经过std::decay的类型,具体没有考证,这是个人猜想。
我想到了这一步也没必要再去死扣源码了,我们的目的已经达成一大半了。
放弃扣这个细节,换成注释的内容后:
main : initial data is main thread's data
thread : initial data is main thread's data
thread : data altered
thread : final data is child thread's data
book is wrong
我们发现子线程中的data仍然是主线程的data,这符合了我们之前的猜想,可能是书本描述有一些问题吧。
忠告
不要暴露handler给其他线程,除非明确持有handler的线程早于持有对象的线程结束。