记录参考:https://www.bilibili.com/video/BV1Yb411L7ak
1、 创建线程:
thread myobj(sp); //sp是一个函数的名,即指向该函数的指针
这句话中创建了一个线程,并且设置线程的起点,即执行的任务。并且开始执行。
2、join
的作用:
阻塞主线程,让主线程等待子线程执行完毕,然后主线程再往下执行。
#include <iostream>
#include <thread>
using namespace std;
void sp() {
cout << "hello"<<endl;
}
int main() {
thread myobj(sp);
myobj.join(); //运行时,先到这里来对主线程进行阻塞,然后再去执行线程的具体内容
cout << "love"<<endl;
}
在如上的多线程中,主线程需要阻塞等待子线程执行结束,然后自己再接着执行并退出,join
由子线程提交给主线程,一个线程一个join
。
如果把join
注释掉,单单是创建一个线程并执行,系统会报错,原因是没有阻塞主线程,就可能会子线程还没执行完,主线程就执行完了准备退出。
3、detach
作用:
detach
的功能和join
对立的,join
使得主线程必须等待子线程执行完再继续,相当于把子线程和主线程捆在一起。而detach
则是把二者分离开来,主线程无需等待子线程执行完成,两者各自执行各自的。
一旦调用了detach
就不能再调用join
了,detach
在调用以后,子线程和主线程分离,主线程不再等待子线程的运行,子线程被转入后台运行。
相当于子线程被C++
运行时库接管了,当子线程执行完,由运行时库清理资源。
分别在主线程和子线程中打印多条记录就能看到效果,由于主线程的几句代码输出太快,子线程有时候还没输出进程就结束了,所以我穿插了一些sleep
:
#include <iostream>
#include <thread>
#include <windows.h>
using namespace std;
void sp() {
cout << "hello1"<<endl;
Sleep(1);
cout << "hello2" << endl;
Sleep(1);
cout << "hello3" << endl;
Sleep(1);
cout << "hello4" << endl;
Sleep(1);
cout << "hello5" << endl;
}
int main() {
thread myobj(sp);
myobj.detach();
cout << "love"<<endl;
Sleep(1);
cout << "love2" << endl;
Sleep(1);
cout << "love3" << endl;
cout << "love4" << endl;
cout << "love5" << endl;
return 0;
}
运行结果:
可以看到其实每次的运行结果都不同,两个线程完全在各自执行自己的代码,互不干扰。甚至子线程输出了一个hello2
还没执行换行操作,主线程就立马输出了一个love2
和换行。而且hello4
和hello5
还没输出,主线程就结束,进程就退出了,子线程也被终止。
由此可以看到,线程在调用了detach
以后,陷入了不可控的状态,我们不能确保这个线程能够被完全执行。创建线程后,可以join
和detach
二选一,但是为了使线程处于一个可控的范围,最好还是使用join
。后面会了解到,detach
会引起非常多的问题。
4、joinable
作用:
判断是否可以成功使用join
或detach
。可以使用这两种时返回true
,不可以时返回false
。
什么情况下会返回false
呢,上面说过,detach
以后就不能设置join
了,此时会返回false
,设置join
后也同样不能再detach
了,不然程序会抛出异常。
所以在设置线程join
时可以:
if(thread1.joinable()){
thread1.join();
}
这样就避免了重复设置导致的程序异常结束。
5、使用类来创建线程:
(1)使用仿函数构造:
#include <iostream>
#include <thread>
using namespace std;
class th {
public:
void operator()() //第一个括号表示重载对象,第二个括号表示重载的参数void //之前记过,类中重载括号实现仿函数
{
cout << "hello"<<endl;
};
};
int main() {
th th;
//由于重载,就有仿函数 th(),作用是输出hello
thread myobj(th);
myobj.join();
cout << "love"<<endl;
}
(2)存在的问题:
#include <iostream>
#include <thread>
using namespace std;
class th {
public:
int& m_i; //定义一个引用
th(int& i):m_i(i){}; //构造函数传入一个引用量 i。初始化列表将引用i赋给m_i,m_i也变成了传入进来的引用对象的引用
void operator()()
{
cout << "m_i的值为" << m_i <<endl;
};
};
int main() {
int m = 3;
th th(m);
thread myobj(th); //此处th是被复制进去的,重载一个上面的构造函数,参数类型为th,会发现构造函数又被执行了一次,因为此处th不是被引用或指针指过去的,而是拷贝过去,创建了一个临时的th对象,所以又触发了一次构造函数
myobj.detach();
cout << "love1" << endl;
}
可以看到这里类的构造函数传入一个引用,将m
传入进去,m_i
就成为了m
的一个引用。由于这里设置的detach
,所以主线程和子线程会各自执行各自的。存在的问题就是,要是我主线程快一步的执行完了,m
被释放掉了,而m_i
又是m
的一个引用,子线程还在执着的打印m_i
,就会产生不可预料的后果。
要么不引用,要么用join
。
6、线程传参:
(1)传递临时对象:
#include <iostream>
#include <thread>
using namespace std;
void fa(const int &i,char *j) {
cout << "i为:" << i << endl
<< "j为:" << j << endl;
}
int main()
{
int a = 12;
char buf[] = "hello";
thread t1(fa,a,buf); //第一个是线程的执行函数,往后的参数是函数的参数
//也可以 thread t1{ fa,a,buf }; 书上使用的是这个花括号
t1.detach();
std::cout << "hello world!"<<endl;
return 0;
}
此处虽然为fa
函数传入a
然后作为引用,但是对a
和函数内的部引用的地址进行监控,会发现地址不一样,实际内部还是进行了一个复制。
既然是复制,上面5 - (2)
中提到的问题怎么会发生呢。回到之前的代码,对参数进行一个监控,就会发现那段代码中传入的m
和类中m_i
的地址是一样的。
事实上是因为之前的参数m
并不是通过线程传参传进去的,而是通过th th(m);
,th
类的构造函数传进去的,所以是实打实的引用,线程中为了避免这一问题,改为了复制一个对象进去,此时函数引用的实际上是复制体,所以这里线程的执行函数中使用引用也是安全的。
(2)存在的问题1:
但是上述代码中,当监控buf
和j
的地址时,会发现buf
和j
的地址相同,代码中使用的指针并不是复制的,所以由于设置了detach
,当主线程结束并释放掉指针时,必定会出现问题:
所以在设置了detach
的线程中最好不要使用引用,绝对不能使用指针。
(3)存在的问题2:
既然无法使用指针,那就把指针替换成引用:
#include <iostream>
#include <thread>
using namespace std;
void fa(const int& i, const string &j) {
//隐式的将变量转换为string,且引用。即由系统来转换其类型。
cout << "i为:" << i << endl
<< "j为:" << j.c_str() << endl;
}
int main()
{
int a = 12;
char buf[] = "hello";
thread t1(fa, a, buf);
t1.detach();
std::cout << "hello world!" << endl;
return 0;
}
由于上面说过,引用以后,实际上线程中是将引用重新复制了一个对象进行引用的,所以即使主线程运行结束释放了相关变量的内存,也不会影响子线程中变量的使用。
事实上,这里还存在一个问题,注意到上面的隐式转换,系统进行隐式转换是需要以buf
为基础进行转换的,但是要是在做转换的时候,buf
被释放掉,传入子线程的参数也会是无可预料的。
为了解决这一问题,可以将类型转换的操作在主线程中进行,在主线程中将类型转为string
,或在传入线程时构造一个临时的string
,即可避免隐式的转换,如:
thread t1(fa, a, string(buf));
在子线程的传参中,应尽量避免隐式转换。
(4)总结:
- 如果传递
int
这种简单的数据类型,建议直接使用值传递。 - 如果传递类对象,应避免隐式类型转换。全部在创建线程中就构建临时对象,然后线程的函数参数中使用线程来接收,否则系统还会构造一次函数对象,造成资源的浪费。
- 建议只是用
join
,不是万不得已不要用detach
,就不存在局部变量失效,导致线程对内存的非法引用。
上面讲的使用引用替换实体,写一下代码:
实体对象
#include <iostream>
#include <thread>
using namespace std;
class A {
public:
A() { cout << "aa"<<endl; }
A(const A& a) { cout << "AA" << endl; }
};
void fa(A a) {
}
int main()
{
int a = 1;
A a1;
thread t1(fa, a1);
t1.join();
return 0;
}
引用
#include <iostream>
#include <thread>
using namespace std;
class A {
public:
A() { cout << "aa"<<endl; }
A(const A& a) { cout << "AA" << endl; }
};
void fa(const A &a) {
}
int main()
{
int a = 1;
A a1;
thread t1(fa, a1);
t1.join();
return 0;
}
可以看到,上面记过了,引用是拷贝一个对象,然后对其引用,所以过程中构造了一次类A
,而直接传递实体对象时,则是进行了两次构造,等于是复制了两次,造成了资源的浪费,所以尽量使用引用。且不管是实体还是引用,都要避免隐式转换。
(5)get_id()获取线程的id:
#include <iostream>
#include <thread>
using namespace std;
void fa() {
cout << "当前线程id:" << this_thread::get_id() << endl;
}
int main()
{
cout <<"主线程id:"<< this_thread::get_id() << endl;
int a = 12;
char buf[] = "hello";
thread t1(fa);
cout << "t1的线程id:" << t1.get_id() << endl;
t1.join();
thread t2(fa);
cout << "t2的线程id:" << t2.get_id() << endl;
t2.join();
return 0;
}
(6)修改线程中的引用:
将线程函数中,引用参数的const
删去,发现程序报错invoke
,这就意味着想要在线程中修改传入的参数是不可行的。
即使使用volatile
和mutable
修饰变量,也难以在线程中对其进行修改。
这一问题可以通过设置std:ref()
来解决,设置以后,线程函数中的引用的const
就可以去掉了:
#include <iostream>
#include <thread>
using namespace std;
void fa(int& i) {
i = 2;
}
int main()
{
int a = 1;
thread t1(fa, std::ref(a)); //std::ref 用于修饰引用传递
t1.join();
cout << "a:" << a;
return 0;
}
要注意的是,前面写过了,如果传递一个引用给线程函数以后,线程函数中的引用实际上是拷贝了一个新的对象来引用,而添加了ref
以后,就变成了真正的引用了,此时如果再用detach
就又陷入到了主线程执行完,释放了内存,造成子线程非法访问,所以如果设置了ref
,一定搭配join
来使用。