线程创建与参数传递
线程的创建
C++11中开始携带标准线程库,便于跨平台程序的移植于编写。一般情况下线程由函数进入,基本的线程创建方式如下:
#include "pch.h"
#include <iostream>
#include <string>
#include <thread> //C++11线程头文件
using namespace std;
void fun()
{
cout << "In child thread: " << std::this_thread::get_id() << endl;
}
int main()
{
thread my_thread(fun);
my_thread.join();
cout << "In main thread: " << std::this_thread::get_id()<< endl;
}
程序运行结果为:
In child thread: 14768
In main thread: 10376
其中,线程由thread my_thread(fun)启动,join()函数的目的是让主线程等待子线程执行完毕,若子线程持续执行,主线程将在join()函数处阻塞直到子线程函数执行完毕。若想在主线程退出后子线程继续执行,可以将my_thread.join()函数替换为my_thread.detach()函数,此时子线程将由系统接管,在后台执行。但由于子线程有时会使用主线程资源,故在主线程结束后子线程有时无法运行或运行出错,所以一般步推荐使用detach函数。
一般来讲,线程创建代码的()内需要是可调用对象,除函数外还有lambda表达式,类内重载的()操作符等,如下:
#include "pch.h"
#include <iostream>
#include <string>
#include <thread> //C++11线程头文件
using namespace std;
auto l = []()
{
cout << "In child thread: " << std::this_thread::get_id() << endl;
};
class A{
public:
void operator() () {
cout << "In child thread: " << std::this_thread::get_id() << endl;
}
};
int main()
{
//thread my_thread(l);
A a;
thread my_thread(a);
my_thread.join();
cout << "In main thread: " << std::this_thread::get_id()<< endl;
}
线程启动函数的参数传递
本处主要介绍基本类型参数与类对象参数的传递方式。
基本对象作为参数
基本对象的函数参数传递方式如下:
void fun(int a) {
a++;
cout << "in the child thread a is: " << a << endl;
}
int main()
{
int a = 2;
thread my_thread(fun, a);
my_thread.join();
cout << "In main thread a is: " << a << endl;
}
输出结果为:
in the child thread a is: 3
In main thread a is: 2
若我们想在线程函数内更改主线程定义的变量a的值,更改fun函数的形参为int &a,即使用变量a的引言,发现程序无法编译通过。必须改为const类型,如下:
void fun(const int &a) { //形参为(int &a)无法编译通过
//a++; //由于const关键字的存在无法对a进行改变
cout << "in the child thread a is: " << a << endl;
}
这是由于线程库为了安全期间,强制限制了无法对线程函数的外的参数进行修改。若是仍然想使用引用的形式对外部传递参数进行修改,可使用std::ref关键字强制转换为引用类型,如下:
void fun(int &a) {
a++;
cout << "in the child thread a is: " << a << endl;
}
int main()
{
int a = 2;
thread my_thread(fun, std::ref(a));
my_thread.join();
cout << "In main thread a is: " << a << endl;
}
输出结果为:
in the child thread a is: 3
In main thread a is: 3
可以看到此时在子线程内部对a值的改变同时影响了主线程中变量a的值。处了上文提及的传递参数的方式外,还可以使用std::bind关键字,如下:
void fun(int a, int b) {
cout << "in the child thread a is: " << a << endl;
cout << "in the child thread b is: " << b << endl;
}
int main()
{
int a = 2;
int b = 2;
thread my_thread(std::bind(fun, a, b));
my_thread.join();
cout << "In main thread a is: " << a << endl;
}
类对象作为参数
相比于基本数据类型,类对象较为复杂,在参数传递时涉及到基本构造函数,拷贝构造函数的执行时间与执行方式,假设代码如下:
class A {
public:
int m_i;
A(int i) :m_i(i) { //普通构造函数
cout << "执行标准构造函数于线程: " << std::this_thread::get_id() << endl;
}
A(const A &a) :m_i(a.m_i) { //拷贝构造函数
cout << "执行拷贝构造函数于线程: " << std::this_thread::get_id() << endl;
}
};
void fun(A a)
{
cout << "子线程执行,a = " << a.m_i << " 子线程id为: " << std::this_thread::get_id() << endl;
}
int main()
{
int i = 2;
A a(i);
thread my_thread(fun, a);
my_thread.join();
cout << "主线程id为: " << std::this_thread::get_id() << endl;
}
执行结果如下:
执行标准构造函数于线程: 9432
执行拷贝构造函数于线程: 9432
执行拷贝构造函数于线程: 16912
子线程执行,a = 2 子线程id为: 16912
主线程id为: 9432
发现与基本函数不同的是,拷贝构造函数执行了两次,即在子线程与主线程分布执行了一次。而基本的函数参数为类对象时拷贝构造函数往往仅执行一次,这就说明在线程开始时,额外执行了一次拷贝构造函数,修改代码为如下:
class A {
public:
int m_i;
A(int i) :m_i(i) { //普通构造函数
cout << "执行标准构造函数于线程: " << std::this_thread::get_id() << endl;
}
A(const A &a) :m_i(a.m_i) { //拷贝构造函数
cout << "执行拷贝构造函数于线程: " << std::this_thread::get_id() << endl;
}
};
void fun(const A &a)
{
cout << "子线程执行,a = " << a.m_i << " 子线程id为: " << std::this_thread::get_id() << endl;
}
int main()
{
int i = 2;
A a(i);
thread my_thread(fun, a);
my_thread.join();
cout << "主线程id为: " << std::this_thread::get_id() << endl;
}
函数fun的参数为类对象的引用,执行结果如下:
执行标准构造函数于线程: 8304
执行拷贝构造函数于线程: 8304
子线程执行,a = 2 子线程id为: 9176
主线程id为: 8304
可以看到使用引用后,主线程内仍然执行了一次拷贝构造函数,即子线程内的对象a还是主线程内a的一个副本,且由于const的存在无法对其进行修改。若仍想要对a副本的内容进行修改,可以使用mutable关键字,如下:
class A {
public:
mutable int m_i;
A(int i) :m_i(i) { //普通构造函数
cout << "执行标准构造函数于线程: " << std::this_thread::get_id() << endl;
}
A(const A &a) :m_i(a.m_i) { //拷贝构造函数
cout << "执行拷贝构造函数于线程: " << std::this_thread::get_id() << endl;
}
void change() const
{ m_i++; }
};
另外,与基础类型类似,可以使用std::ref关键字来进行强制引用处理,不执行拷贝构造函数,子线程可直接对主线程内对象进行修改。
类内成员函数作为线程起始函数
线程起始函数可以为类内成员函数,如下:
class A {
public:
int m_i;
A(){}
A(int i) :m_i(i) { //普通构造函数
cout << "执行标准构造函数于线程: " << std::this_thread::get_id() << endl;
}
A(const A &a) :m_i(a.m_i) { //拷贝构造函数
cout << "执行拷贝构造函数于线程: " << std::this_thread::get_id() << endl;
}
void fun(int a, int b){}
};
int main()
{
int i = 2;
int p,q = 0;
A a(i);
thread my_thread(&A::fun, a, p, q);
my_thread.join();
cout << "主线程id为: " << std::this_thread::get_id() << endl;
}
detach()函数的缺点
上文提到过,detach函数会使子线程脱离主线程,由系统接管在后台执行。然而很多场合子线程会使用主线程的资源来正常运行,特别当线程起始函数传递的参数为主线程变量的引用时,主线程结束后其内部变量将会被销毁,导致子线程也无法正确执行。实际过程中detach函数很少使用,因此本处先不做介绍。