线程函数传参详解

四、线程传参详解

本节主要详细记录std::thread的构造函数,就是函数的形参列表。

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

thread类中的detach()方法是一个大坑,它将简单的东西复杂化,需要谨慎处理!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <thread>

using namespace std;

void myprint(const int& i, char* pmybuf) { // 这里尽管是按照引用传递,但是观察内存地址可以发现这里内部一定做了复制
cout << i << endl; // 按照指针传递,pmybuf的地址和传入进来的参数地址一样
cout << pmybuf << endl;
}

int main()
{
int mvar = 1; // 创建临时对象
int &mvary = mvar;
char mybuf[] = "This is a test";

thread mytobj(myprint, mvar, mybuf); // i并不是mvar的引用,实际上是按照值传递;但是mybuf是按照地址传递!
mytobj.join();
// mytobj.detach(); // 尽管主线程detach了子线程,那么子线程中用i值也是安全的,但是使用mybuf一定会出问题!
cout << "I Love China" << endl;
return 0;
}

所以传给thread的可调用对象中,不推荐使用引用传递(引用的真实含义被覆盖了),同时绝对不可以用指针!如何安全的将字符串传递进来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void myprint(const int& i, const string& pmybuf) { 
cout << i << endl;
cout << pmybuf << endl;
}
int main()
{
int mvar = 1;
int &mvary = mvar;
char mybuf[] = "This is a test";
thread mytobj(myprint, mvar, mybuf); // 这里存在一个隐式的类型转换char-->string
// thread mytobj(myprint, mvar, string(mybuf)); // 一个可行的方法!!!!!
mytobj.detach(); // 一个潜在的风险是转换的时机可能发生在mybuf[]被回收后
// mytobj.join();
cout << "I Love China" << endl;
return 0;
}

在创建线程的同时构造临时对象的方法传递参数是可行的,他可以保证在主线程结束前将myprint()的第二个参数构造出来,直接使用隐式类型转换再detach()则不能保证。上述的程序会先调用string中的构造函数创建一个临时对象,接着并不是引用这个临时对象,而是调用copy构造函数又复制出来一个对象给子线程使用!

对于使用detach()的情况:

  • 若果传递内置类型的变量,建议都是按照值传递,不推荐使用引用,坚决不能用指针。
  • 如果传递类对象,避免使用隐式类型转换。全部都在创建线程这一行创建出临时对象来,并且在函数参数中使用引用来接收参数。
  • 除非万不得已!是使用join()就不会出现局部变量失效导致线程对内存非法访问的情形。

2、线程ID

每个线程,不管是主线程还是子线程实际上都对应了一个数字,可以使用C++标准库中的std::this_thread::get_id()来获取。利用这个ID信息我们就可以追踪临时对象是在哪一个线程中被构造出来的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include "pch.h"
#include <iostream>
#include <thread>
using namespace std;
class TestThread {
public:
TestThread(int i) :data_(i) {
cout << "构造函数被执行" << this <<"创建此对象的ID是:"<<std::this_thread::get_id()<< endl;
}
TestThread(const TestThread& testthread) :data_(testthread.data_) {
cout << "Copy构造函数被执行" << this <<"执行copy操作的线程ID是:"<<std::this_thread::get_id()<< endl;
}
~TestThread() {
cout << "析构函数被执行" << this <<"执行析构的线程ID是:"<<std::this_thread::get_id()<< endl;
}
void thread_work(int num){
cout<<"类TestThread中的线程函数"<<this<<"所在线程ID"<<std::this_thread::get_id()<<endl;
}
public:
int data_;
};
void MyTestThread(const TestThread& testthread) { // 参数不使用值传递,不然会构建出3个对象(执行2次copy构造函数)
cout << "子线程的ID是:" <<std::this_thread::get_id()<< endl;
}
int main()
{
cout << "主线程的ID是:" << std::this_thread::get_id() << endl;
int mvar = 1;
// thread mytobj1(MyTestThread,mvar); // 这种情况是在子线程中构造的临时对象
thread mytobj1(MyTestThread,TestThread(mvar)); // 这种情况是在主线程中构造的临时对象
mytobj1.join();
return 0;
}
  • 当使用隐式类型转换时,临时对象是在子线程中被构建的,这就会导致detach()出现问题。(主线程执行完,子线程无法构造了)
  • 当创建线程使用临时对象时,临时对象是在主线程中被构建出来的,这样确保使用子线程不会出问题。

3、std::ref()函数

前边说过,尽管函数void myprint(const int& i, const string& pmybuf)是按照引用的方式传递的,但是编译器调用copy构造函数又创建了一个对象,这就导致在子线程中对pmybuf的更改不会反馈到主线程中(const 限定符也不允许我们这么做,如果去掉const限定符号,编译又会出错)。

这时候使用std::ref()函数就可以实现引用的真正用处了:

1
2
3
4
5
6
7
8
9
10
11
void MyTestThread(TestThread& testthread) {          // 这时候const可以去掉
cout << "子线程的ID是:" <<std::this_thread::get_id()<< endl;
}
int main()
{
cout << "主线程的ID是:" << std::this_thread::get_id() << endl;
TestThread aaa(20);
thread mytobj1(MyTestThread, std::ref(aaa)); // 不会再copy出来一份了
mytobj1.join();
return 0;
}

4、任意一个成员函数作为线程函数

调用类中的任意一个成员函数,可以用以下的写法来实现:

1
2
3
4
5
TestThread aaa(20);
thread mytobj1(&ThreadTest::thread_work, aaa, 15);
// thread mytobj1(&ThreadTest::thread_work, std::ref(aaa), 15); //这时候使用detach就会出现问题
// thread mytobj1(&ThreadTest::thread_work, &aaa, 15); // 等同于上一种写法
myobj1.join();
听说打赏我的人,最后都找到了真爱。