C++ 多线程学习笔记(3):线程传参的进一步分析

  • 根据前一篇文章的分析:C++ 多线程学习笔记(2):线程启动、结束、创建线程方法:C++多线程中使用detach可能导致问题。
  • 假设在构造子线程时,传入参数引用了主线程的变量,而且子线程创建后调用detach() 了,如果主线程先执行完,这个被引用的变量就会被回收,而此时子线程(没执行完)仍在引用这块内存空间,会导致不可预料的结果
  • 这里针对这个问题进行进一步分析

1. 传递临时对象作为线程参数

(1)要避免的陷阱

  • 范例
  #include <iostream>
  #include <thread>
  using namespace std;
  
  //子线程起始函数
  void myprint(const int &i, char *pmybuf)
  {
  	cout << i << endl;
  	cout << pmybuf << endl;
  	return;
  }
  
  int main()
  {
  	int mvar = 1;
  	int &mvary = mvar;
  	char mybuf[] = "this is a test!";
  
  	thread mytobj(myprint, mvar, mybuf);
  	mytobj.join();
  	cout << "主线程" << endl;
  
  	return 0;
  }
  
  /*------运行结果--------
  1
  this is a test!
  主线程
  */
  • 上面的程序没啥问题,但如果把join换成detach,就会出现问题
  #include <iostream>
  #include <thread>
  using namespace std;
  
  //子线程起始函数
  void myprint(const int &i, char *pmybuf)
  {
  	cout << i << endl;
  	cout << pmybuf << endl;
  	return;
  }
  
  int main()
  {
  	int mvar = 1;
  	int &mvary = mvar;
  	char mybuf[] = "this is a test!";
  
  	thread mytobj(myprint, mvar, mybuf);
  	mytobj.detach();
  	cout << "主线程" << endl;
  
  	return 0;
  }
  • debug可以发现,实参 mvar 的地址和 myprint 函数形参的 &i 的地址不同,说明thread构造函数内部应当是进行了复制,&i这个引用是假的引用,本质还是把 mvar 的值复制过去的

    • 因此,即使主线程中detach了子线程,子线程中的引用还是安全的
    • 但是,依然不推荐这样写!
  • debug可以发现,实参 mybuf 的地址和 myprint 函数形参的 *pmybuf 的地址相同,说明指针传递是传地址的

    • 因此,如果主线程中detach了子线程,子线程中的指针是不安全的,如果主线程先于子线程结束并撤销内存,子线程会发生不可预测的结果
  • 针对上面发现的引用问题,我们可以做以下修改

  //子线程起始函数
  void myprint(const int i, const string &pmybuf)
  {
  	cout << i << endl;
  	cout << pmybuf.c_str() << endl;
  	return;
  }
  
  int main()
  {
  	int mvar = 1;
  	int &mvary = mvar;
  	char mybuf[] = "this is a test!";
  
  	thread mytobj(myprint, mvar, mybuf);
  	mytobj.detach();
  	cout << "主线程" << endl;
  
  	return 0;
  }
  • 第一个形参变成传值了,肯定没问题

  • 第二个从指针变成string引用了,因为实参是char数组,传参时势必要隐式构造一个string,然后引用的时候还会构造一个临时string的副本(按上面的分析)。总之这样就不会引用实参的 mybuf。debug也可验证,实参的 mybuf 地址和形参的 pmybuf 地址不同,所以可以认为这么改就一定安全吗?

  • 事实上,不行!!可以验证,有可能会发生这种情况:主线程已经结束并被回收了,才拿mybuf指针去转string准备开始执行子线程的情况发生,这会使程序陷入不可预料中!

  • 最终修改版

  //子线程起始函数
  void myprint(const int i, const string &pmybuf)
  {
  	cout << i << endl;
  	cout << pmybuf.c_str() << endl;
  	return;
  }
  
  int main()
  {
  	int mvar = 1;
  	int &mvary = mvar;
  	char mybuf[] = "this is a test!";
  
  	thread mytobj(myprint, mvar, string(mybuf));	//这里改成生成临时string!
  	mytobj.detach();
  	cout << "主线程" << endl;
  
  	return 0;
  }
  • 这种方法可以保证安全,形参pmybuf引用了临时string,之后thread 传引用时内部又复制了一遍(上面分析了)。经过测试,可以保证主线程结束前临时string一定生成。
  • 小结:
    • 当子线程的起始函数使用非基本类型的A类对象的引用作为形参时,只要用临时构造的A类对象作为参数传给thread构造函数(不能用隐式类型转换),就可以保证一定能在主线程结束之前把子线程形参的这个引用的真实对象构造出来(先构造临时对象,再拷贝一次),从而确保即使detach子线程也能安全运行。

(2)总结

  • 如果使用 detach 子线程

    • 若传递int这种简单变量,建议都传值

    • 如果传递类对象,避免隐式类型转换。全部在thread构造函数调用时就构造临时对象,在函数形参中用引用来接

  • 建议不使用detach,只用join,这样就不存在局部变量失效导致线程对内存的非法引用问题

2. 临时对象作为线程参数(续)

(1)线程id概念

  • id是个数字,每个线程(不管主线程还是子线程)都有一个线程id
  • 不同的线程,线程id不同
  • 可以用std::this_thread::get_id()来获取线程id

(2)临时对象构造时间抓捕

  • 经过实验发现
    • 传递类对象,使用隐式类型转换时,临时对象和拷贝对象都是在子线程构造的
    • 传递类对象,使用临时对象传参时,临时对象和拷贝对象都是在主线程构造的
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章