最近工作当中对于在c++11多线程的使用过程当中总有许多的疑惑,且遇到了很多的坑,今天特意进行总结一番。
一:传递临时对象作为线程参数
范例一:
#include <iostream>
#include <thread>
using namespace std;
void func(int& i, char* buf)
{
printf("&i = %d\n", &i);
printf("buf = %p\n", buf);
cout << buf << endl;
}
int main()
{
int var = 1;
int& mvar = var;
printf("&var = %d\n", &var);
printf("&mvar = %d\n", &mvar);
char mbuf[] = "abucdf";
printf("&mbuf = %p\n", mbuf);
std::thread mthread(func, std::ref(var), mbuf);
mthread.join();
//mthread.detach();
}
从输出结果可以看出,子线程当中的i正是绑定的主线程当中的var,同理,buf指向的内存与mbuf也是一样的,如果这里不是用join而是使用detach会带来很多的隐患问题,如果主线程已经执行完毕,那么变量var不就被系统回收了吗?那么我们在子线程当中使用i,就会存在隐患。所以,尽量不要在线程传参当中使用指针与引用。我们针对上面的范例进行一下优化
范例二:
#include <iostream>
#include <thread>
using namespace std;
void func(const int& i, const string& buf)
{
printf("&i = %d\n", &i);
printf("buf = %p\n", buf);
cout << buf << endl;
}
int main()
{
int var = 1;
int& mvar = var;
printf("&var = %d\n", &var);
printf("&mvar = %d\n", &mvar);
char mbuf[] = "abucdf";
printf("&mbuf = %p\n", mbuf);
std::thread mthread(func, var, mbuf);
mthread.join();
//mthread.detach();
}
从输出结果来看子线程使用的两个参数是重新拷贝了一份进去,这样就不必担心范例一存在的问题了。
这里的话,随便提一下,就是尽量使用const引用,对于thread来说,这样才会产生临时对象,而如果不用const好像会编译错误,当然,真正的引用要使用std::ref才行。
看到这里我们貌似觉得没有问题了?实际上,还存在一个问题,上面范例二当中是希望mbuf转化为string,那么这个转换是在什么时候,或者说在那里进行的?如果主线程都执行完毕了,mbuf都回收了,那么这个时候在转换还有什么意义?
我们再针对范例二进行优化
范例三:
#include <iostream>
#include <thread>
using namespace std;
void func(const int& i, const string& buf)
{
printf("&i = %d\n", &i);
printf("buf = %p\n", buf);
cout << buf << endl;
}
int main()
{
int var = 1;
int& mvar = var;
printf("&var = %d\n", &var);
printf("&mvar = %d\n", &mvar);
char mbuf[] = "abucdf";
printf("&mbuf = %p\n", mbuf);
std::thread mthread(func, var, string(mbuf));
mthread.join();
//mthread.detach();
}
我们看到范例三用string(mbuf)这个生成了一个临时对象,那么这种方式对于使用detach来说是否可以避免隐患问题?我们继续探讨:
范例四:
#include <iostream>
#include <thread>
using namespace std;
class A
{
public:
A(int a) :m_i(a)
{
std::cout << "构造函数执行了" << this<<std::endl;
}
A(const A& a)
{
std::cout << "拷贝构造函数执行了" << this <<std::endl;
}
~A()
{
std::cout << "析构函数执行了" << std::endl;
}
int m_i;
};
void func(int i, const A& buf)
{
std::cout << i << std::endl;
std::cout << &buf << std::endl;
}
int main()
{
int a = 1;
int senvar = 2;
std::thread mthread(func, a, senvar);
mthread.join();
//mthread.detach();
}
通过结果可以知道,子线程当中的buf确实是通过senvar构造的,但是如果换成detach会怎么样?
可能一句语句也不会输出了,本来是希望用一个senvar变量去构造一个临时对象的,然后给子线程使用,但是现在还没构造出来这个变量,主线程就执行完毕了。。
我们换一种方式进行研究。
#include <iostream>
#include <thread>
using namespace std;
class A
{
public:
A(int a) :m_i(a)
{
std::cout << "构造函数执行了" << this<<std::endl;
}
A(const A& a)
{
std::cout << "拷贝构造函数执行了" << this <<std::endl;
}
~A()
{
std::cout << "析构函数执行了" << this <<std::endl;
}
int m_i;
};
void func(int i, const A& buf)
{
std::cout << i << std::endl;
std::cout << &buf << std::endl;
}
int main()
{
int a = 1;
int senvar = 2;
std::thread mthread(func, a, A(senvar));
//mthread.join();
mthread.detach();
}
使用了A(senvar)以后我们就能确保在主线程当中就把这个临时对象构造出来了,这样主线程即使执行完毕,也不会影响子线程的使用。至于到底什么时候进行的构造,这个可以把线程id打印出来观察。
总结:所以对于像一些简单的类型,比如int,尽量用值传递,如果使用类对象作为传递参数,也要尽量避免发生隐式类型转换,尽量在创建线程这一行代码就把临时对象构造出来,而线程入口函数要尽量使用const引用做为形参,这个与thread内部原理有关,如果不用引用可能会多调用几次拷贝构造函数。