文章目录
detach大坑
1、传递临时对象作为线程参数
在上篇文章中,我们简单介绍了使用仿函数构建线程时,不能使用引用、指针等来初始化类,防止在detach()时,主线程把局部变量内存释放,本节来详细介绍下,除仿函数对象外,在使用detach()还要注意哪些bug。
1.1、子线程入口函数的参数地址问题
#include <iostream>
#include <thread>
#include <string>
using namespace std;
/*void myPrint(const int& i, const char* name){
// 如果线程从主线程detach了
// 在子线程的函数入口处,使用引用来传递参数,但是该引用不是真正的引用,不再是地址传递,实际上为值传递,重新拷贝一个值并重新申请一块内存给了子线程。
// 即使主线程运行完毕了,子线程用i仍然是安全的,但仍不推荐传递引用
// 推荐改为const int i
cout << "id is: " << i << endl;
// 与引用不同,使用指针来传递参数,该指针仍然指向的主线程中申请的地址,
// 也就是说name还是指向原来的字符串,那么,当主线程中的的局部变量被释放,子线程对应的内存也就不安全了
cout << "my name is: " << name << endl;
}*/
// 故而推荐的写法就是:不推荐使用引用,并且绝对不能使用指针
// 那么如何传递变量和字符串呢?必须添加const
void myPrint(const int i, const string& name){
cout << "id is: " << i << endl;
cout << "my name is: " << name << endl;
}
int main(int argc, char* argv[]){
thread myThreaad(myPrint, 1006, "twlcy");
myThreaad.detach();
cout << "I love China!" << endl;
return 0;
}
通过上述的代码的展示,我们把避免了引用和指针问题带来的内存回收问题:
1、对于整型、浮点型等使用引用,在子线程中实际是值传递,不再是地址传递,这使得线程运行比较安全,但是,避开引用,会更加安全;
2、对于字符串,使用指针,会导致传递的参数的指针指向主线程中局部变量的地址,那么我们就利用string类型的引用来代替指针(当然最好也就不使用引用),使得线程相对安全。
1.2、数据类型隐式转换问题
也就是说上述解决方式相对安全,但是对于char类型隐式地转换为string,仍存在一些问题。就是说存在一定地可能性使得char类型数据被主线程释放掉了,那么又怎么使用该数据转为string类型,这就会导致程序出现bug。
解决方式:只要用临时构造的类对象作为参数传递给线程,那么就一定能够在主线程执行完毕前把线程函数的第二个参数构建出来,从而确保即便detach()了,子线程也能安全运行。
#include <iostream>
#include <thread>
#include <string>
using namespace std;
void myPrint(const int i, const string& name)
{
cout << i << endl;
cout << name << endl;
}
int main(int argc, char* argv[])
{
int mvar = 1;
char mybuf[] = "this is a test";
//推荐先创建一个string地临时对象就绝对安全了。。。。
thread myThread(myPrint, mvar, string(mybuf))
myThread.detach();
cout << "I love China!" << endl;
}
这里证明一下上述结论:
也就是创建了一个类,并且进行创建
#include <iostream>
#include <thread>
#include <string>
using namespace std;
class Student{
public:
Student(int id):m_Id(id){
cout << "Constructor is running!" << endl;
}
Student(const Student& stu):m_Id(stu.m_Id){
cout << "Copy-constructor is running!" << endl;
}
~Student(){
cout << "Destructor is running!" << endl;
}
private:
int m_Id;
};
// 故而推荐的写法就是:不推荐使用引用,并且绝对不能使用指针
// 那么如何传递变量和字符串呢?必须添加const
void myPrint(const int i, const Student& name){
cout << "id is: " << i << endl;
}
int main(int argc, char* argv[]){
int id = 1006;
thread myThreaad(myPrint, 1006, id); // 这里把id隐式地转换成Student类
myThreaad.join();
// myThreaad.detach();
cout << "I love China!" << endl;
return 0;
}
使用join()先看看正常地隐式转换:
那么,在使用detach()函数看看隐式没有转换成功地情况:
这里不打印主程序地 “I love China!”,使得主程序结束地更快。
可以看到,执行了两次都没有来的及执行隐式转换类对象。
那么使用临时对象呢?
thread myThreaad(myPrint, 1006, id);
那么可以清晰地看到,构造函数被执行了,拷贝构造也正常执行。当然,一般我们传递类对象的时候,都是先进行初始化的,所以临时对象也不太常用,这里主要是注意char类型的转string的情况。
这里使用vscode调试的时候,出现了两次拷贝构造(按理来讲应该只会执行一次拷贝构造),vs2019出现一次拷贝构造,暂时还理不清楚。标记为一个bug。
总结:
(a)若传递int这种简单类型的参数,建议都是值传递,不要用引用,防止节外生枝
(b)如果传递类对象,避免隐式类型转换。全部都在创建线程这一行就构建出临时对象来,然后在函数参数里用引用来接;否则系统还会多构造一次对象,浪费。
(c)建议不使用detach(),只使用join();这样就不存在局部变量失效导致线程对内存的非法引用的问题;
2、临时对象作线程参数再分析
2.1、线程id概念
id是个数字,每个线程(不管是主线程还是子线程)实际上都对应一个数字,而且每个线程对应的这个线程id可以用C++标准库里的函数来获取。
2.2、使用线程id进行分析
针对上述的bug,利用线程的id来查看是哪些线程调用了类的构造函数以及拷贝构造,这里为了显示代码的思绪清晰使用了join()函数进行分析。
std::this_thread::get_id() // 获取线程的id
#include <iostream>
#include <thread>
#include <string>
using namespace std;
class Student{
public:
Student(int id):m_Id(id){
cout << "Constructor is running!" << "this thread id is: " << std::this_thread::get_id() << endl;
}
Student(const Student& stu):m_Id(stu.m_Id){
cout << "Copy-constructor is running!" << "this thread id is: " << std::this_thread::get_id() << endl;
}
~Student(){
cout << "Destructor is running!" << "this thread id is: " << std::this_thread::get_id() << endl;
}
private:
int m_Id;
};
void myPrint(const Student& name){
cout << "child thread id is: " << std::this_thread::get_id() << endl;
}
int main(int argc, char* argv[]){
cout << "main thread id is: " << std::this_thread::get_id() << endl;
int id = 1006;
//Student stu(id);
thread myThreaad(myPrint, id); // 这里把id隐式地转换成Student类
myThreaad.join();
//myThreaad.detach();
//cout << "I love China!" << endl;
return 0;
}
不使用临时对象的情况:
使用临时对象(VS2019):
总结:在向子线程的入口函数传递参数时,使用直接传递或者引用(不能使用指针),无论传递的是简单的数据类型还是类的临时对象,那么都属于 值传递,也就是说,属于深拷贝(类对象的深拷贝就是执行拷贝构造)。深拷贝都发生在当前线程内,而非当前线程的子线程。
那么这就引出一个问题,也就是无论是直接传递参数还是引用,thread内部都处理成值传递,那么在子线程内对参数进行修改,就不会影响当前线程内的数据的值。
注意:在子线程入口函数的传递使用引用方式,必须加上const修饰,保证线程安全,否者编译器报错。如:
void myPrint(const int& id, const Student& name) {
cout << "child thread id is: " << std::this_thread::get_id() << endl;
}
3、传递类对象、智能指针为线程参数
3.1、地址传递类对象
上述文章中已经说明,为了保证线程运行安全,使用的引用方式被thread修改为了值传递,那么在子线程中进行对数据的修改不会影响主线程的数据,但是如果需要进行这种操作呢?
这时候就是用到了std::ref()函数了,这会告诉编译器,现在现在不再使用值传递而是地址传递了。
使用语法:
thread myThreaad(myPrint, std::ref(stu)); // std::ref(stu)为子线程入口函数的参数
例如:
#include <iostream>
#include <thread>
#include <string>
using namespace std;
class Student {
public:
Student(int id) :m_Id(id) {
cout << "Constructor is running!" << " this thread id is: " << std::this_thread::get_id() << endl;
}
Student(const Student& stu) :m_Id(stu.m_Id) {
cout << "Copy-constructor is running!" << " this thread id is: " << std::this_thread::get_id() << endl;
}
~Student() {
cout << "Destructor is running!" << " this thread id is: " << std::this_thread::get_id() << endl;
}
void setId(const int id) {
this->m_Id = id;
}
int getId() {
return this->m_Id;
}
private:
int m_Id;
};
void myPrint(Student& stu) { // const不再添加
cout << "child thread id is: " << std::this_thread::get_id() << endl;
stu.setId(10);
}
int main(int argc, char* argv[]) {
cout << "main thread id is: " << std::this_thread::get_id() << endl;
Student stu(1006);
cout << "Original data is; " << stu.getId() << endl;
thread myThreaad(myPrint, std::ref(stu)); // 这里把id隐式地转换成Student类
myThreaad.join();
cout << "Modified data: " << stu.getId() << endl;
cout << "I love China!" << endl;
return 0;
}
注意:在使用std::ref()时,是地址传递,那么类对象就不会再执行拷贝构造了;而且入口函数的参数列表也不再使用const修饰
不能使用detach()
3.2、传递指针指针
使用std::move()
#include <iostream>
#include <thread>
#include <string>
using namespace std;
class Student {
public:
Student(int id) :m_Id(id) {
cout << "Constructor is running!" << " this thread id is: " << std::this_thread::get_id() << endl;
}
Student(const Student& stu) :m_Id(stu.m_Id) {
cout << "Copy-constructor is running!" << " this thread id is: " << std::this_thread::get_id() << endl;
}
~Student() {
cout << "Destructor is running!" << " this thread id is: " << std::this_thread::get_id() << endl;
}
void setId(const int id) {
this->m_Id = id;
}
int getId() {
return this->m_Id;
}
private:
int m_Id;
};
void myPrint(unique_ptr<Student> stu) {
cout << "child thread id is: " << std::this_thread::get_id() << endl;
stu->setId(10);
}
int main(int argc, char* argv[]) {
cout << "main thread id is: " << std::this_thread::get_id() << endl;
unique_ptr<Student> pStu(new Student(1006)); //创建一个智能指针
cout << "Original data is; " << pStu->getId() << endl;
//独占式指针只能通过std::move()才可以传递给另一个指针
//传递后pStu就指向空,新的stu指向原来的内存
//所以这时就不能用detach了,因为如果主线程先执行完,ptn指向的对象就被释放了
thread myThreaad(myPrint, std::move(pStu)); // 这里把id隐式地转换成Student类
myThreaad.join();
// 同样的,也不能再使用pStu了
// cout << "Modified data: " << pStu->getId() << endl;
cout << "I love China!" << endl;
return 0;
}
总结:智能指针传递同样没有拷贝构造,独占式指针只能通过std::move()才可以传递给另一个指针,传递后pStu就指向空,新的stu指向原来的内存,原始指针不能再使用。
这里不能使用detach()
4、成员函数指针作线程函数
#include <iostream>
#include <thread>
#include <string>
using namespace std;
class Student {
public:
Student(int id) :m_Id(id) {
cout << "Constructor is running!" << " this thread id is: " << std::this_thread::get_id() << endl;
}
Student(const Student& stu) :m_Id(stu.m_Id) {
cout << "Copy-constructor is running!" << " this thread id is: " << std::this_thread::get_id() << endl;
}
~Student() {
cout << "Destructor is running!" << " this thread id is: " << std::this_thread::get_id() << endl;
}
void setId(const int id) {
this->m_Id = id;
}
int getId() {
return this->m_Id;
}
void thread_work(int num) {
cout << "child thread is running!" << " num = " << num << " this thread id is: " << std::this_thread::get_id() << endl;
}
private:
int m_Id;
};
int main(int argc, char* argv[]) {
cout << "main thread id is: " << std::this_thread::get_id() << endl;
Student stu(1006);
thread myThread(&Student::thread_work, stu, 100); //
// myThread.join();
myThread.detach();
cout << "I love China!" << endl;
return 0;
}
注意:上述程序中,入口函数时值传递,可以使用detach()
5、总结
入口函数无论是普通函数、仿函数、成员函数中的参数列表中包含真正的引用或指针时,也就是std::ref()或std::move()子线程不使用detach()。否者参数列表中均为值传递(普通传递或者即使写成引用都会在thread类内部处理成值传递),可以使用detach(),以保证线程安全。