c++ 学习之 多线程(四)再谈 join 与 detach

c++ 学习之 多线程(四)再谈 join 与 detach

前言

在之前的学习中,我们初步了解了join与detach的作用,但是当实际应用的时候,有很多地方需要注意,不然总会发生一些意想不到的事情发生,下面我们来说一说join与detach的坑。

正文

1.实参传递问题
问题场景:
当我们用主线程中的局部变量当作实参去初始化thread对象后,调用detach将子线程与主线程分离开,并且主线程在子线程之前结束。
后果:
主线程结束后,主线程内的局部变量将会被释放,但是子线程中仍在使用相应内存空间的内容,这就会出现问题。
解决办法:
用值传递来初始化thread对象,这样相应的变量会被拷贝一份传进子线程中,其拷贝出来的变量的作用域为子线程,主线程结束后,资源释放不会对其造成影响。
若必须传递实参时,应确保子线程在主线程之前结束,或使用join(),让主线程等待子线程结束。

问题场景:
当我们向线程中传递引用时,往往起不到引用的效果,这时候我们需要注意我们传递的是否真的是这个变量的引用呢?

#include<stdio.h>
#include<thread>
using namespace std;
void fun(const int &a)
{
	  printf("子线程中a的地址 :%p\n", &a);
}
int main()
{
	 int a = 0;
	 printf("主线程中a的地址 :%p\n", &a);
	 thread t(fun,a);
	 t.join();
}

输出结果:
主线程中a的地址 :00F3FAB8
子线程中a的地址 :0113EFC8

可以看到这样传递的引用并不是真正的引用,还是一个值传递。
解决办法:

#include<stdio.h>
#include<thread>
using namespace std;
void fun(const int &a)
{
	  printf("子线程中a的地址 :%p\n", &a);
}
int main()
{
	  int a = 0;
	  printf("主线程中a的地址 :%p\n", &a);
	  thread t(fun,ref(a));
	  t.join();
}


输出结果:
主线程中a的地址 :004FFE1C
子线程中a的地址 :004FFE1C
使用std::ref(),按引用传递。

2.值传递问题
正常的值传递不必多说,这里举一个不正常的值传递。
当我用字符串char*来初始thread对象的时候,如果直接将字符串传递进去,这样又会产生了实参传递,当调用detach的时候还会发生上述的主线程资源释放问题。

#include<thread>
#include<stdio.h>
using namespace std;
void fun(char *str1)
{ 
   printf("%s\n",str1);
}
int main()
{
    char str[] = "i love china!";
    thread t(fun,str);
    t.joni();
}

在这里插入图片描述在这里插入图片描述为了调试我们调用join(),实际这个问题只有在调用detach()时才会发生。
可以看到,str 和 str1的地址是一样的。
那么我怎么能让其值传递呢?最开始想到的解决办法是用一个string变量来当参数类型,这样传递char*变量时,就会隐式的创建一个string变量。

#include<thread>
#include<stdio.h>
#include<string>
using namespace std;
void fun(const string &str)
{
   printf("%s\n", str.c_str());
}
int main()
{
  char str[] = "i love china!";
  printf("%s\n", str);
  thread t(fun,str);
  t. detach();
}

这样的作法看似解决了字符串值传递的问题,但实际上仍然存在着隐患。我们这样做,是为了用char*生成string对象,但是如果生成时string对象的时间在主线程结束之后,这样的作法又会被主线程的资源释放所影响。所以我们还需要进一步解决。

#include<thread>
#include<stdio.h>
#include<string>
using namespace std;
void fun(const string &str)
{
  printf("%s\n", str.c_str());
}
int main()
{
  char str[] = "i love china!";
  printf("%s\n", str);
  thread t(fun,string(str));
  t. detach();
}

经过多方面的查证,用这种产生临时对象的方法,可以在构造thread对象的时候产生string对象,这样可以避免以上问题。
我们可以自己定义一个类来求证一下。

#include<thread>
#include<stdio.h>
#include<string>
using namespace std;
class A
{
public:
 int i;
 A(int a):i(a)
 {
  printf("调用了构造函数 %p %d\n",this,this_thread::get_id()); 
 }
 A(const A& a):i(a.i)
 {
  printf("调用了拷贝构造函数 %p %d\n",this,this_thread::get_id());
 }
 ~A()
 {
  printf("调用了析构函数 %p %d\n",this,this_thread::get_id());
 }
};
void fun(const A &a)
{
 printf("%d\n", a.i);
}
int main()
{   
 int a = 10;
 thread t(fun,a);
 t.detach();
}

这样测试的结果是控制台上什么都没有输出就结束了,也正是咱们预想的那样,用隐式的转换可能会发生在转换之前主线程结束的问题。那用临时对象的方式来测试一下:

#include<thread>
#include<stdio.h>
#include<string>
using namespace std;
class A
{
public:
 int i;
 A(int a):i(a)
 {
  printf("调用了构造函数 %p %d\n",this,this_thread::get_id()); 
 }
 A(const A& a):i(a.i)
 {
  printf("调用了拷贝构造函数 %p %d\n",this,this_thread::get_id());
 }
 ~A()
 {
  printf("调用了析构函数 %p %d\n",this,this_thread::get_id());
 }
};
void fun(const A &a)
{
 printf("%d\n", a.i);
}
int main()
{   
 int a = 10;
 thread t(fun,A(a));
 t.detach();
}

输出结果:
调用了构造函数 00BBFA34 13216
调用了拷贝构造函数 010EF3D0 13216
调用了析构函数 00BBFA34 13216
这里第一个构造的调用是临时对象A(a)的构造,拷贝构造是将临时对象值传递拷贝进函数中,然后临时对象被析构,所有的操作都是在主线程中完成的,主线程结束,也就是说这些参数在子线程开始之前就已经构造完毕了,就不会发生上述问题l了。

解释一下为什么函数参数要使用引用。
上述说过在不使用std::ref传参的情况下,引用使假引用,实质还是值传递,但是当使用临时对象传参时,如果不用引用当参数类型,会在开始执行子线程时再次调用一次拷贝构造,也就是调用了两次拷贝构造,虽然作用没什么影响,但是会影响执行效率,所以用引用作为参数类型可以避免调用两次拷贝构造。

发布了10 篇原创文章 · 获赞 6 · 访问量 412

猜你喜欢

转载自blog.csdn.net/weixin_45074185/article/details/104484198