C++:std::thread arguments must be invocable after conversion to rvalues

引言

最近在看《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的线程早于持有对象的线程结束。

  • 16
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值