前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >UNIX(多线程):08---线程传参详解,detach()陷阱,成员函数做线程函数

UNIX(多线程):08---线程传参详解,detach()陷阱,成员函数做线程函数

作者头像
用户3479834
发布2021-02-03 14:26:52
8000
发布2021-02-03 14:26:52
举报
文章被收录于专栏:游戏开发司机

线程传参详解,detach()陷阱,成员函数做线程函数

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

【引例】

代码语言:javascript
复制
代码语言:javascript
复制
#include <iostream>
#include <string>
#include <thread>
using namespace std;
void myprint(const int& i, char* pmybuf ) {
cout << i << endl;
cout << pmybuf << endl;
return;
}
int main()
{
int val = 1;
int& val_y = val;
char buf[] = "This is a Test!";
thread mythread(myprint, val, buf);  //传递参数
mythread.join();
//主线程执行
std::cout << "主线程收尾" << std::endl;
return 0;
}

要避免的陷阱(解释1)

  • 如果上面使用detach,子线程和主线程分别执行,由于传递给myprint的是val的引用,如果主线程结束,会不会给子线程造成影响?
    • 答案是不会,虽然我们传给子线程看上去是引用传递,实际上是将val的值拷贝给了 函数参数 i,可以通过调试程序,查看各个变量的内存地址,就会发现 val 和 val_y内存地址相同,但是 i 的地址与val地址不同,这就说明了实际上不是引用传递,是值拷贝传递。建议使用detach的时候,线程函数,不要写成引用传递。
  • 针对线程函数第二个参数 pmybuf,通过调试查看地址,发现主线程中的buf地址和线程中的pmybuf内存地址相同,如果使用detach,就会产生问题。
  • 所以使用detach的时候不要使用引用传递,尤其是不要使用指针(绝对有问题),这会引起错误。
  • 解决方式
代码语言:javascript
复制
void myprint(const int i, const string & pmybuf ) {cout << i << endl;cout << pmybuf.c_str() << endl;return;}
  • 字符数组转string,隐式类型转换。
  • 创建临时对象,最终赋值给string,这样就是不同的内存了。

要避免的陷阱(解释2)

代码语言:javascript
复制
 thread mythread(myprint, val, buf);  //传递参数
  • 代码执行到这一行,mybuf究竟是什么时候传递给string的?是否main函数都执行完了(此时mybuf被回收了),才把mybuf往string转。事实上这种方式是有这样的风险。
  • 更安全的做法(进行显示类型转换),将线程的pmybuf绑定到buf转换成的string临时对象。
代码语言:javascript
复制
 thread mythread(myprint, val, string(buf) );  //传递参数
  • 这里直接将mybuf转换成string临时对象,这是一个可以保证在线程中肯定有效的对象。
  • string(buf)什么时候转换?是不是main函数执行完了才开始转,这样还是使用了被系统回收的内存。
  • 事实上这样转没有问题了。
  • 下面进行证明:
代码语言:javascript
复制
#include <iostream>
#include <string>
#include <thread>
using namespace std;
class A {
public:
int m_i;
//类型转换构造函数,可以把一个int转换为类A对象
A(int i) :m_i(i) { cout << "A::A(int i)函数执行了" << endl; }
A(const A &other) :m_i(other.m_i) { cout << "A::A(const A &other)函数执行了" << endl; }
~A() { cout << "A:: ~A()函数执行了" << endl; }
};
void myprint(const int i, const A & p_a ) {
cout << &p_a << endl;  //这里打印p_a对象的地址
return;
}
int main()
{
int m_val = 1;
int n_val = 22;
thread mythread(myprint, m_val, n_val);  //希望n_val转换成A类对象传给myprint第二个参数
mythread.join();
//主线程执行
std::cout << "主线程收尾" << std::endl;
return 0;
}
  • 上面说明可以通过一个整型构造一个A类型的对象。
  • 如果将上面的join改成detach,则结果如下:
  • 由输出可知该构造是发生在main函数执行完毕后才开始的。主线程退出后n_val的内存空间被回收了,此时还用n_val(无效了)去构造A类对象,这会导致一些未定义的行为。
  • 我们期望n_val能够通过A类的类型转换构造函数构造出对象,但是遗憾的发现直到主线程退出了都没构造出我们想要的对象然后传给子线程。
  • 我们使用显示地进行转换,构造出临时对象,然后调用拷贝构造函数将临时对象拷贝给线程函数的第二个参数p_a.
代码语言:javascript
复制
thread mythread(myprint, m_val, A(n_val));
  • 输出:
  • 在整个main函数执行完毕之前,肯定已经构造出了临时对象并且传递到线程中去了。
  • 即证明了在创建线程的同时构造临时对象的方法传递参数是可行的。

总结

  • 若传递int这种简单类型参数,建议都是值传递,不要用引用,防止节外生枝。
  • 如果传递类对象,避免隐式类型转换。全部都在创建线程这一行就构建出临时对象来,然后在线程函参数里,用引用来接(否则系统还会构造临时对象来接,构造三次)。
  • 建议不使用detach(),只使用join(),这样就不存在局部变量失效导致线程对内存的非法引用问题。

临时对象作为线程参数继续讲

线程id概念

  • id是个数字,每个线程(不管是主线程还是子线程)实际上都对应着一个数字,而且每个线程对应的这个数字都不同。
    • 也就是说,不同的线程,它的线程id(数字)必然是不同。
  • 线程id可以用c++标准库里的函数来获取。通过 std::this_thread::get_id() 来获取。

临时对象构造时机抓捕

代码语言:javascript
复制
#include <iostream>
#include <string>
#include <thread>
using namespace std;
class A {
public:
int m_i;
//类型转换构造函数,可以把一个int转换为类A对象
A(int i) :m_i(i) { cout << "A::A(int i)函数执行了" << this << "  ThreadId  " \
<< std::this_thread::get_id()<< endl; }
A(const A &other) :m_i(other.m_i) { cout << "A::A(const A &other)函数执行了" << this \
<< "  ThreadId  " << std::this_thread::get_id() << endl; }
~A() { cout << "A:: ~A()函数执行了" << this << "  ThreadId  "  \
<< std::this_thread::get_id() << endl; }
};
void myprint(const A  &p_a ) {
cout << "子线程myprint()参数地址:" << &p_a << "  ThreadId  " \
<< std::this_thread::get_id() << endl;
return;
}
int main()
{
cout << "主线程ID" << std::this_thread::get_id() << endl;
int n_val = 69;
thread mythread(myprint, n_val);  //希望n_val转换成A类对象传给myprint第二个参数
mythread.join();
//主线程执行
std::cout << "主线程收尾" << std::endl;
return 0;
}
  • 注意到n_val构造成A类对象是发生在子线程中的。如果detach就出问题了。
代码语言:javascript
复制
thread mythread(myprint, A(n_val));
  • 使用显示类型转换,创建临时对象的方式,可以主线程执行完毕之前将临时对象构造出来,然后拷贝到子线程当中去。
  • 如果线程函数中使用值拷贝,不用引用方式:
代码语言:javascript
复制
void myprint(const A  p_a )
  • 在子线程中多执行了一次拷贝构造函数,所以建议在类作为参数传递时,使用引用方式传递(虽然写的是引用方式,但是实际上是按值拷贝方式处理)。

传递类对象、智能指针作为线程参数

在线程中修改变量的值不会影响到主线程。

  • 将类A的成员变量m_i改成mutable。
代码语言:javascript
复制
#include <iostream>
#include <string>
#include <thread>
using namespace std;
class A {
public:
mutable int m_i;
//类型转换构造函数,可以把一个int转换为类A对象
A(int i) :m_i(i) { cout << "A::A(int i)函数执行了" << this << "  ThreadId  " \
<< std::this_thread::get_id()<< endl; }
A(const A &other) :m_i(other.m_i) { cout << "A::A(const A &other)函数执行了" << this \
<< "  ThreadId  " << std::this_thread::get_id() << endl; }
~A() { cout << "A:: ~A()函数执行了" << this << "  ThreadId  "  \
<< std::this_thread::get_id() << endl; }
};
void myprint(const A  &p_a ) {
p_a.m_i = 89;
cout << "子线程myprint()参数地址:" << &p_a << "  ThreadId  " \
<< std::this_thread::get_id() << endl;
return;
}
int main()
{
cout << "主线程ID" << std::this_thread::get_id() << endl;
A a(1);
thread mythread(myprint, a);
mythread.join();
//主线程执行
std::cout << "主线程结束" << std::endl;
return 0;
}
  • 虽然传进去的是引用,但是线程中对m_i的值进行修改,不会影响到main函数中的a对象的m_i的值。
  • 线程中对象p_a信息:
  • 在线程中对m_i的发生修改后,此时对象a的信息:
  • 虽然对象a是以引用传递的方式传给p_a,但是这个过程是拷贝构造的过程,两个对象的内存地址不同。

【std::ref()】

  • 如果需要真正的把对象引用传递到线程函数当中,就需要使用 std::ref()
代码语言:javascript
复制
#include <iostream>
#include <string>
#include <thread>
using namespace std;
class A {
public:
int m_i;
//类型转换构造函数,可以把一个int转换为类A对象
A(int i) :m_i(i) { cout << "A::A(int i)函数执行了"<< endl; }
A(const A &other) :m_i(other.m_i) { cout << "A::A(const A &other)函数执行了"  << endl; }
~A() { cout << "A:: ~A()函数执行了"  << endl; }
};
void myprint(A  &p_a ) {
p_a.m_i = 89;
cout << "子线程myprint()执行了"  << endl;
return;
}
int main()
{
A a(1);
thread mythread(myprint, std::ref(a));
mythread.join();
//主线程执行
std::cout << "主线程结束" << std::endl;
return 0;
}
  • 调试时线程中对象p_a信息:
  • 在线程中对m_i的发生修改后,此时对象a的信息:
  • 最终输出:
  • 使用了std::ref() 拷贝构造函数就没有了,且两个对象地址相同,实现真正的引用传递。

智能指针,想从一个堆栈到另一个堆栈,需要使用std::move()

代码语言:javascript
复制
#include <iostream>
#include <string>
#include <thread>
using namespace std;
void myprint(unique_ptr<int> ptr_u) {
cout << "子线程myprint()执行了"  << endl;
return;
}
int main()
{
unique_ptr<int> m_ptr(new int(100));
thread mythread(myprint, std::move(m_ptr));
mythread.join();
//主线程执行
std::cout << "主线程结束" << std::endl;
return 0;
}
  • 调试查看m_ptr信息:
  • 调试查看ptr_u信息:
  • 两者指向的地址相同。
  • 注意:如果这里使用detach,就很危险,因为线程中的智能指针指向的是主线程中的一块内存,当主线程执行完毕而子线程中的智能指针还指向这块内存就会出错。

用成员函数指针做线程函数

代码语言:javascript
复制
#include <iostream>
#include <string>
#include <thread>
using namespace std;
class A {
public:
int m_i;
//类型转换构造函数,可以把一个int转换为类A对象
A(int i) :m_i(i) { cout << "A::A(int i)函数执行了"<< endl; }
A(const A &other) :m_i(other.m_i) { cout << "A::A(const A &other)函数执行了"  << endl; }
void func(int i) { cout << "A::func(int i)函数执行了" \
<< "  i =  " << i << endl; }
~A() { cout << "A:: ~A()函数执行了"  << endl; }
};
int main()
{
A a_obj(11);
thread mythread(&A::func, a_obj, 233);
mythread.join();
//主线程执行
std::cout << "主线程结束" << std::endl;
return 0;
}

【注】类对象使用引用方式传递

代码语言:javascript
复制
thread mythread(&A::func, &a_obj, 233);
代码语言:javascript
复制
thread mythread(&A::func, std::ref(a_obj), 233);
  • 使用引用或者std::ref不会调用拷贝构造函数,这时使用detach就要注意了。

【operator()带参数】

代码语言:javascript
复制
#include <iostream>
#include <string>
#include <thread>
using namespace std;
class A {
public:
int m_i;
//类型转换构造函数,可以把一个int转换为类A对象
A(int i) :m_i(i) { cout << "A::A(int i)函数执行了"<< endl; }
A(const A &other) :m_i(other.m_i) { cout << "A::A(const A &other)函数执行了"  << endl; }
void operator()(int i) { cout << "A::operator()执行了" \
<< "  i =  " << i << endl; }
~A() { cout << "A:: ~A()函数执行了"  << endl; }
};
int main()
{
A a_obj(11);
thread mythread(a_obj, 666);
mythread.join();
//主线程执行
std::cout << "主线程结束" << std::endl;
return 0;
}
  • 改用std::ref() 传递可调用对象
代码语言:javascript
复制
thread mythread(std::ref(a_obj), 999);
  • 少了拷贝构造函数进行资源复制,使用detach要小心。
  • 改用 引用& 传递可调用对象
代码语言:javascript
复制
thread mythread(&a_obj, 999);
  • 这种方式不可以,程序报错。

使用detach注意事项小结

  • 验证传入的参数(类对象)究竟是在主线程中构造完成后传进去的,还是在子线程中构造创建的。使用线程id 加类的构造函数与拷贝构造函数进行测试。
  • 注意是不是使用了std::ref()进行传参。
  • 关注是不是主线程中的资源值拷贝方式给了子线程。
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-01-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 游戏开发司机 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 线程传参详解,detach()陷阱,成员函数做线程函数
    • 传递临时对象作为线程参数
      • 要避免的陷阱(解释1)
      • 要避免的陷阱(解释2)
      • 总结
    • 临时对象作为线程参数继续讲
      • 线程id概念
      • 临时对象构造时机抓捕
    • 传递类对象、智能指针作为线程参数
      • 用成员函数指针做线程函数
        • 使用detach注意事项小结
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档