线程函数传递临时对象
首先,我们先来看一下线程函数传递的临时对象,引用传递与函数指针传递;我们知道使用detach()
函数后,主线程与子线程脱离关联,各自独立运行,主线程运行结束,可能会导致子线程数据非法访问的问题。我们看下面的错误示例:
#include <iostream>
#include <thread>
#include <Windows.h>
void PrintMsg(const int& var, char* flow)
{
std::cout << "var address is : " << var << std::endl;
std::cout << "flow address is : " << flow << std::endl; // ①
}
int main(void)
{
std::cout << "the main thread starts." << std::endl;
int mvar = 1;
char tmp_flow[] = "I learn C++ Thread.";
std::thread print(PrintMsg, mvar, tmp_flow); // ②
//print.join();
print.detach();
std::cout << "the main thread end." << std::endl;
return 0;
}
上述代码中线程PrintMsg()
函数分别传递的是引用mvar和tmp_flow的数据参数,我们知道使用线程阻塞join()
函数不会出现任何问题。但是,如果使用detach()
函数,脱离关联独自运行。那么,这里可能会涉及到非法访问。我们首先看一下函数的地址:
从上面调试地址值可以看到,整形变量的引用作为函数参数传递,但是实际上进行地址拷贝操作。虽然不会出现非法引用,但是并不建议这么操作,还是建议传值。参数指针传递,tmp_flow
与flow
的地址完全一致,说明线程函数执行时候读取tmp_flow
的地址所有数据值。那么,这里就会存在问题:如果主线程执行完毕,子线程还在运行。主线程的tmp_flow
已经被释放,这个时候子线程还需要访问数据,就会造成崩溃。因此,使用detach()
函数,一定要关注线程函数入参的生命周期。或者,尽量不要使用detach(),使用线程join()
来阻塞,保证线程函数参数的生命周期。
线程函数传类对象
#include <iostream>
#include <thread>
class A
{
public:
A(int m_i) :m_i_(m_i)
{
std::cout << "A::A()构造函数执行." << std::endl;
}
A(const A& a) :m_i_(a.m_i_)
{
std::cout << "A::A(const A& a)拷贝构造函数执行." << std::endl;
}
~A()
{
std::cout << "A::~A()析构函数执行." << std::endl;
}
public:
int m_i_;
};
void print(const A& a) // ③
//void print(const A a) // ④
{
std::cout << "the A address is " << &a << std::endl;
}
int main(void)
{
int m_i = 3;
A a(m_i);
std::cout << "the main thread A address is " << &a << std::endl;
std::thread print(print, a); // ①
//std::thread print(print, std::ref(a)); // ②
print.join();
//print.detach();
return 0;
}
简单依据上述代码进行分析,想必都注意到代码中的①②③④的符号,分别对应不同的使用情况导致不同的输出结果。
- 使用③①组合,输出的结果为:
A::A()构造函数执行.
the main thread A address is 010FFA84
A::A(const A& a)拷贝构造函数执行.
the A address is 0155E550
A::~A()析构函数执行.
A::~A()析构函数执行.
- 使用④①组合,输出的结果为:
A::A()构造函数执行.
the main thread A address is 00CFFA40
A::A(const A& a)拷贝构造函数执行.
A::A(const A& a)拷贝构造函数执行.
the A address is 00E3F960
A::~A()析构函数执行.
A::~A()析构函数执行.
A::~A()析构函数执行.
- 使用③②情况,输出的结果为:
A::A()构造函数执行.
the main thread A address is 006FF870
the A address is 006FF870
A::~A()析构函数执行.
- 使用④②情况下,输出的结果为:
A::A()构造函数执行.
the main thread A address is 006FFCA8
A::A(const A& a)拷贝构造函数执行.
the A address is 007FF508
A::~A()析构函数执行.
A::~A()析构函数执行.
分析上述输出:我们通过上述几个不同的输出对比可以看到,④①组合调用了两次拷贝构造函数,③②一次拷贝构造函数也没有调用。因此③②组合效率最快,④①组合效率最差。其中③①与④②分别调用了一次拷贝构造函数,但是它们两个调用拷贝构造函数是不同的阶段。
- ③①产生的拷贝构造函数是启动线程,线程触发的一次类对象拷贝;
- ④②产生的拷贝构造函数是print()函数入参以传值模式,触发的拷贝类对象;
- ④①产生的拷贝构造函数分别是启动线程与函数传值导致两次拷贝类对象;
- ③②将线程触发改为类对象引用,线程函数入参也为引用,因此省去两次拷贝构造;
每多一次拷贝构造,分别对应多一次析构。
上述代码在使用线程阻塞join()
的情况下,并不会出现任何问题。但是,一旦使用detach()
函数,使用引用②将会导致不安全情况出现,如果主线程执行结束,类A对象已经析构,这个时候子线程还需要引用类A对象的数据,这将造成索引失败,导致崩溃。
线程函数传智能指针
关于智能指针作为线程函数参数传递时候,必须使用移动构造函数的方式进行传参,std::move()
将智能指针采取右值引用的权限授权给线程函数ptr。如果使用std::ref()
来引用其智能指针地址,不涉及拷贝问题。但是,需要时刻关注生命周期。
#include <iostream>
#include <thread>
// shared_ptr unique_ptr
void use_smart_pointer(std::unique_ptr<int>& ptr) // std::ref() 这里必须使用&
{
std::cout << "use_smart_pointer()." << std::endl;
}
int main(void)
{
std::cout << "main thread start." << std::endl;
std::unique_ptr<int> m_ptr(new int(10));
std::thread testUniquePtr(use_smart_pointer, std::move(m_ptr));
//std::thread testUniquePtr(use_smart_pointer, std::ref(m_ptr));
testUniquePtr.join();
std::cout << "main thread end." << std::endl;
return 0;
}
线程函数传递成员函数
接下来我们看一下,线程函数调用类成员函数的方式。我们通过获取线程的id来判断线程处于主线程还是子线程。调用方式:std::thread temp(&A::work, a, parameters)
。其中A
为类,a
为类对象,work
为类A
的成员函数,后面的parameters
为work()
函数的入参。
#include <iostream>
#include <thread>
class A
{
public:
A(int m_i) :m_i_(m_i)
{
std::cout << "A::A()构造函数执行." << this << "| get thread_id " << std::this_thread::get_id() << std::endl;
}
A(const A& a) :m_i_(a.m_i_)
{
std::cout << "A::A(const A& a)拷贝构造函数执行." << this << "| get thread_id " << std::this_thread::get_id() << std::endl;
}
~A()
{
std::cout << "A::~A()析构函数执行." << this << "| get thread_id " << std::this_thread::get_id() << std::endl;
}
void work(int n)
{
std::cout << "work函数执行 " << this << "| get thread_id " << std::this_thread::get_id() << std::endl;
}
public:
int m_i_;
};
int main(void)
{
int m_i = 3;
A a(m_i);
std::cout << "主线程 ID " << "| get thread_id " << std::this_thread::get_id() << std::endl;
std::thread print(&A::work, a, m_i); // ②
print.join();
//print.detach();
return 0;
}
输出结果如下:我们可以看到构造函数、拷贝构造函数都在主线程id下面进行操作,说明调用线程函数时候,执行在主线程上生成临时对象,然后子线程进行执行work()
函数。
A::A()构造函数执行.004FFE40| get thread_id 17136
主线程 ID | get thread_id 17136
A::A(const A& a)拷贝构造函数执行.009CE6F4| get thread_id 17136
work函数执行 009CE6F4| get thread_id 17776
A::~A()析构函数执行.009CE6F4| get thread_id 17776
A::~A()析构函数执行.004FFE40| get thread_id 17136
线程函数传递参数std::ref()
试想一下,我们启动一个新的线程函数,可能会需要进行对该线程执行结束后的参数调用。那么,我们就需要使用std::ref()
来进行操作。前面的例子中我们已经看到使用std::ref()
只是引用了地址,并未进行拷贝操作。
#include <iostream>
#include <thread>
#include <Windows.h>
void PrintMsg(int& var, char* flow)
{
var = 5;
std::cout << "var value is : " << var << std::endl;
}
int main(void)
{
std::cout << "the main thread starts." << std::endl;
int mvar = 1;
char tmp_flow[] = "I learn C++ Thread.";
std::cout << "mvar value before is " << mvar << std::endl;
std::thread print(PrintMsg, std::ref(mvar), tmp_flow);
print.join();
//print.detach();
std::cout << "mvar value after is " << mvar << std::endl;
std::cout << "the main thread end." << std::endl;
return 0;
}
上述的一个小程序,主要就是想介绍一下,如果主线程需要使用子线程PrintMsg()
执行后的mvar
结果,为下面继续操作,那么我们就要使用std::ref()
来进行操作。mvar
的值经过子线程执行后,值发生改变。
the main thread starts.
mvar value before is 1
var value is : 5
mvar value after is 5
the main thread end.
小结
线程传参经验小结:
- 线程函数参数为单个类型值情况下(非结构体、类等),建议使用传值模式,不要使用指针或引用;
- 线程函数参数为类对象情况下,建议使用引用方式,避免多次调用拷贝构造函数。不过需要注意类对象的生命周期;
- 如果你想要通过线程函数改变参数值,来为后续使用的话,建议使用
std::ref()
来进行真正的引用; - 使用
detach()
函数将线程关联脱离时候,必须小心数据的生命周期。 - 一般线程调用主要使用阻塞
join()
函数,以此来避免使用detach()
函数导致的生命周期问题。但是,如果使用detach()
则必须时刻关注子线程调用的数据是否会随着主线程结束而释放导致子线程非法访问。
参考文献
https://zh.cppreference.com/w/cpp/thread/thread