头文件<thread>
创建thread
对象
thread
有三个构造函数,分别是默认构造函数,复制构造函数,和带参数的构造函数.
创建和使用一个thread实例类似如下:
void print()
{
cout << "新线程启动" << endl;
cout << "新线程结束" << endl;
}
int main()
{
thread t(print);
t.join();
cout << "主线程结束\n";
cout << endl;
}
其中第一个参数是可调用对象(函数、成员函数、函数对象、lambda)
当创建出一个thread对象时,C++就自动将它尽可能启动于一个新线程,如果没有成功,则抛出异常std::system_error
.
成员函数join()
的使用方法
join
中文直译意思是加入、结合、连接.
顾名思义,使用这个成员函数,子线程会和主线程汇合,也就是说主线程会等待子线程结束,在子线程结束之前,主线程不会结束.
使用Join
,我们可以确保子线程能够完全的运行,如果主线程走的快,主线程就会等待子线程的完毕,最后主线程和子线程都能运行完毕.
成员函数detach()
的使用方法
detach
中文直译意思是分离、分开.
顾名思义,我们各走各的,你走你的,我走我的。也就是说主线程不会等待子线程,即使主线程结束的时候子线程还没有结束。
一但使用detach
,与主线程相关的thread对象就会失去与主线程的关联,此时这个子线程就会驻留在后台运行,相当于被C++运行时库接管,当这个子线程执行完毕后,由运行时库负责清理该线程相关的资源.
因此使用detach会带来很多的问题,比如
:
void print()
{
cout << "子线程1" << endl;
cout << "子线程2" << endl;
cout << "子线程3" << endl;
cout << "子线程4" << endl;
cout << "子线程5" << endl;
cout << "子线程6" << endl;
cout << "子线程7" << endl;
cout << "子线程8" << endl;
cout << "子线程9" << endl;
}
int main()
{
thread t(print);
t.detach();
cout << "主线程1\n";
cout << "主线程2\n";
cout << "主线程3\n";
cout << "主线程4\n";
cout << "主线程5\n";
cout << "主线程6\n";
cout << endl;
}
可以看到每次运行都会有不同的结果
…
…
传递临时对象作为线程参数带来的问题
当可调用对象使用引用时,还会带来其他的结果:
class A
{
public:
int #
public:
A(int &i) :num(i) {}
void operator()()
{
cout << num << endl;
cout << num << endl;
cout << num << endl;
}
};
int main()
{
int n = 1;
A a(n);
thread t(a);
t.detach();
cout << "hello world" << endl;
return 0;
}
可以看到,当主线程结束的时候,此时引用的n已经被消耗了,此时访问引用就会带来无法预料的结果.
还有个疑问:当主线程结束时,上面的a对象不是已经被销毁了吗?为什么还能调用?
实际上,子线程调用的对象实际上是复制到子线程中的,当主线程结束以后原来的对象确实已经被销毁了,但是复制的对象没有消耗,还是能使用的。
我们可以使用一个例子来论证一下:
class A
{
public:
int #
public:
A(int &i) :num(i) { cout << "带参构造函数调用" << endl; }
A(const A& other) :num(other.num) { cout << "复制构造函数调用" << endl; }
~A()
{
cout << "析构函数调用" << endl;
}
void operator()()
{
cout << num << endl;
cout << num << endl;
cout << num << endl;
}
};
int main()
{
int n = 1;
A a(n);
thread t(a);
t.join();
cout << "hello world" << endl;
return 0;
}
当调用参数时,会出现你想象不到的结果:
void print(const int & i, char * p)
{
cout << i << endl;
cout << p << endl;
return;
}
int main()
{
int n = 1;
int& m = n;
char c[] = "test!";
thread t(print, m, c);
t.detach();
cout << "hello world" << endl;
return 0;
}
得到的地址如下:
&n 0x00affa2c {1}
&m 0x00affa2c {1}
&i 0x00f0f444 {1}
c 0x00f3f978 "test!"
p 0x00f3f978 "test!"
可以知道i并不是n的引用,实际上是值传递,即使主线程detach了子线程,那么子线程中用i值仍然是安全的。
但是p和c却是相同的地址,当主线程完成后 c的内存就被释放,这个时候访问p就会有很大的危险
如果换成string呢会怎么样?
void print(const int & i, const string & p)
{
cout << i << endl;
cout << p.c_str() << endl;
return;
}
int main()
{
int n = 1;
int& m = n;
char c[] = "test!";
thread t(print, m, c);
t.detach();
cout << "hello world" << endl;
return 0;
}
c 0x0056fd20 "test!" char[6]
&p 0x0079f7b0 "test!" const std::basic_string<char,std::char_traits<char>,std::allocator<char> > *
可以看到用string得到的结果是不一样的,看上去string没有引用主线程的c,好像安全了。问题是真的安全了?
有一个问题很有可能被忽略了,c是什么时候转换成string p的呢
?很有可能c被回收了然后还没有进行转换
解决办法:使用临时对象,避免隐式转换:
thread t(print, m, string(c));
验证如下:
class A
{
public:
int num;
public:
A(int i) :num(i) { cout << "带参构造函数调用" << endl; }
A(const A& other) :num(other.num) { cout << "复制构造函数调用" << endl; }
~A()
{
cout << "析构函数调用" << endl;
}
void operator()()
{
cout << num << endl;
cout << num << endl;
cout << num << endl;
}
};
void print(const int & i, const A & p)
{
cout << i << endl;
cout << p.num << endl;
return;
}
int main()
{
int n = 1;
int& m = n;
char c[] = "test!";
thread t(print, m, A(10));
t.detach();
cout << "hello world" << endl;
return 0;
}
总结:
1.如果传递int这种简单类型,建议都是按值传递,不要用引用.
2.如果传递类对象,避免隐式类型转换,在创建线程这一行就构建出临时对象,然后参数使用引用,不然会在多构造出一个对象。
3.避免麻烦,尽量别使用detach
线程ID
线程id是一个数字,每一个线程都有自己的id.
使用std::this_thread::get_id()
来获取,
class A
{
private:
int m;
public:
A(int a) :m(a) { cout << "带参数构造函数执行,线程ID是: " << this_thread::get_id() << endl; }
A(const A& other) :m(other.m) {
cout << "复制构造函数执行,线程ID是: " << this_thread::get_id() << endl;
}
A() {cout << "默认构造函数执行,线程ID是: " << this_thread::get_id() << endl; }
~A() { cout << "析构函数执行,线程ID是: " << this_thread::get_id() << endl; }
};
void print(const A & a)
{
cout << "子线程print的参数地址是:" << &a << ",子线程id是: " << this_thread::get_id() << endl;
}
int main()
{
cout << "主线程id是: " << this_thread::get_id() << endl;
int n = 1;
thread m(print, n);
m.join();
}
可以看到A对象是在子线程中构造的,如果使用detach,就会出现主线程结束而子线程还没构造对象的情况,此时调用n就会出现不可预料的结果
##如果换成临时对象呢?
thread m(print, A(n));
扫描二维码关注公众号,回复:
11339234 查看本文章
可以看到主线程进行了带参构造和复制构造,就不会有上面的问题
std::ref
函数
class A
{
public:
mutable int m;
public:
A(int a) :m(a) { cout << "带参数构造函数执行,线程ID是: " << this_thread::get_id() << endl; }
A(const A& other) :m(other.m) {
cout << "复制构造函数执行,线程ID是: " << this_thread::get_id() << endl;
}
A() {cout << "默认构造函数执行,线程ID是: " << this_thread::get_id() << endl; }
~A() { cout << "析构函数执行,线程ID是: " << this_thread::get_id() << endl; }
};
void print(const A & a)
{
a.m = 100;
cout << "子线程print的参数地址是:" << &a << ",子线程id是: " << this_thread::get_id() << endl;
}
int main()
{
cout << "主线程id是: " << this_thread::get_id() << endl;
int n = 1;
A a(n);
thread m(print, a);
m.join();
}
// &a 0x004ffe24 {m=1 } A *
// &a 0x0081fc50 {m=100 } const A *
可以看到即使传递是引用,子线程的修改不会对主线程造成任何影响。如果真的想修改需要使用std::ref函数
void print(const A & temp)
{
temp.m = 100;
cout << "子线程print的参数地址是:" << &temp << ",子线程id是: " << this_thread::get_id() << endl;
}
int main()
{
cout << "主线程id是: " << this_thread::get_id() << endl;
int n = 1;
A a(n);
thread m(print, ref(a));
m.join();
}
// &temp 0x004ff9c0 {m=100 } const A *
// &a 0x004ff9c0 {m=100 } A *
智能指针做参数
可以看到无法编译通过,解决办法:使用std::move
注意:这种情况不要使用detach了,会有危险。
…
…
成员函数做线程参数
class A
{
public:
mutable int m;
public:
A(int a) :m(a) { cout << "带参数构造函数执行,线程ID是: " << this_thread::get_id() << endl; }
A(const A& other) :m(other.m) {
cout << "复制构造函数执行,线程ID是: " << this_thread::get_id() << endl;
}
A() {cout << "默认构造函数执行,线程ID是: " << this_thread::get_id() << endl; }
~A() { cout << "析构函数执行,线程ID是: " << this_thread::get_id() << endl; }
void print(int n)
{
cout << "子线程print执行 " << this << " 线程id = " << this_thread::get_id() << endl;
}
};
void print(unique_ptr<int>p)
{
}
int main()
{
cout << "主线程id是: " << this_thread::get_id() << endl;
A myobj(10);
thread t(&A::print, myobj, 15);
t.join();
}