前言
接上文,这次对C++多线程和并发有了一些粗浅的理解,上一篇文章如下:
C++多线程的Demo(一)_c++ demo-CSDN博客
详细讲解join()和detach():
每一个程序至少拥有一个线程,那就是执行main()函数的主线程,而多线程则是出现两个或两个以上的线程并行运行,即主线程和子线程在同一时间段同时运行。而在这个过程中会出现几种情况:
主线程先运行结束
子线程先运行结束
主子线程同时结束
在一些情况下需要在子线程结束后主线程才能结束,而一些情况则不需要等待,但需注意一点,并不是主线程结束了其他子线程就立即停止,其他子线程会进入后台运行!!!这一点很多人根本发不现,包括原来的自己,以为主线程停了,子线程就自己消失了,其实并不是。在C++层面,子线程仍然在运行,只是对于不同的操作系统,会有一些优化,所以子线程会被优化掉,但是为了跨平台和安全性,还是应该注意。(当然,如果是detach的,C++运行时库也会在主线程退出以后,正确回收相关的资源,但是这不是我们自己不管理的理由)
join()示例
join()函数是一个等待线程完成函数,主线程需要等待子线程运行结束了才可以结束。这样操作比较安全,但是有个致命的问题是,一定要注意join()的位置。如果在创建子线程以后,立马join(),然后再跟上主线程的任务,那么join()以后,程序会等待子线程执行,然后再执行主线程的任务,那么无非就是把主线程阻塞住,然后去执行子线程的部分任务,那这样和把所有任务都交给主线程有啥区别吗?多此一举不是?例如以下:
#include <iostream>
#include <thread>
using namespace std;
void func()
{
for(int i = -10; i > -20; i--)
{
cout << "from func():" << i << endl;
}
}
int main() //主线程
{
cout << "mian()" << endl;
cout << "mian()" << endl;
cout << "mian()" << endl;
thread t(func); //子线程
t.join(); //等待子线程结束后才进入主线程
cout << "mian()" << endl;
cout << "mian()" << endl;
cout << "mian()" << endl;
return 0;
}
所以,为了能够保证主线程和子线程确实是分开执行的,应该把join()放到尽可能靠后的位置:
#include <iostream>
#include <thread>
using namespace std;
void func()
{
for(int i = 10; i > 0; i--)
{
cout << "from func():" << i << endl;
_sleep(1);
}
}
int main() //主线程
{
thread t(func); //子线程
for(int i = 0; i < 10; i++)
{
cout << "mian():" << i << endl;
_sleep(1);
}
t.join(); //等待子线程结束
return 0;
}
detach()示例
detach()称为分离线程函数,使用detach()函数会让线程在后台运行,即说明主线程不会等待子线程运行结束才结束。通常称分离线程为守护线程(daemon threads),UNIX中守护线程是指,没有任何显式的用户接口,并在后台运行的线程。这种线程的特点就是长时间运行;线程的生命周期可能会从某一个应用起始到结束,可能会在后台监视文件系统,还有可能对缓存进行清理,亦或对数据结构进行优化。
#include <iostream>
#include <thread>
using namespace std;
void func()
{
for(int i = 10; i > 0; i--)
{
cout << "from func():" << i << endl;
_sleep(1);
}
}
int main() //主线程
{
thread t(func); //子线程
t.detach(); //分离子线程
for(int i = 0; i < 10; i++)
{
cout << "mian():" << i << endl;
_sleep(1);
}
return 0;
}
这时候我们发现,在创建完子线程以后,直接detach()的话,和join想要的结果相同。
注意1:无限循环的子线程问题
需要注意的是,以上这个例子是一个很快就执行完的子线程任务,如果是一个无限循环的任务,就不一定了,既然是分离,那么主线程关闭与否,并不影响子线程的运行状态。虽然在某些情况下,操作系统可能会在主线程结束后清理所有相关的资源,包括子线程。但是,这并不是C++标准所保证的行为,它完全依赖于操作系统的实现。
所以,对于这种情况,建议使用join()
来等待子线程结束,或者在子线程中设置某种形式的退出条件,以便它可以检测到主线程已经结束,并相应地结束自己的执行。
使用标志位去控制的方法如下:
#include <iostream>
#include <thread>
using namespace std;
void func(bool & flag) // 这里使用引用传递
{
int i=0;
while(flag)
{
cout << "from func():" << i++ << endl;
_sleep(100);
}
cout << "from func() end!!!" << endl;
}
int main() //主线程
{
bool f = true;
thread t(func, std::ref(f)); //子线程
t.detach(); //分离子线程
for(int i = 0; i < 50; i++)
{
cout << "mian():" << i << endl;
_sleep(100);
}
f=false;
_sleep(100); // 最后需要等待100ms,不然的话,子线程无法执行end那句话
return 0;
}
注意2:主线程报错退出的问题
我们想象这么一种情况,就是子线程运行的好好的,也设置了停止条件,但是主线程里面,如果执行某个语句报错了咋办嘞? 就无法运行到那个标志位改变了,这样子线程也是停不了的:
#include <iostream>
#include <thread>
#include <exception>
using namespace std;
void func(bool & flag) // 这里使用引用
{
int i=0;
while(flag)
{
cout << "from func():" << i++ << endl;
_sleep(100);
}
cout << "from func() end!!!" << endl;
}
int main() //主线程
{
bool f = true;
thread t(func, std::ref(f)); //子线程,传递参数
t.detach(); //分离子线程
for(int i = 0; i <50 ; i++)
{
cout << "mian():" << i << endl;
if(i==25) throw std::runtime_error("An error occurred!"); // 手动抛出异常
_sleep(100);
}
f=false;
_sleep(100); // 最后需要等待100ms,不然的话,子线程无法执行end那句话
return 0;
}
解决办法就是加一个try 和catch:
伪代码如下:
thread t(func, std::ref(f)); //子线程,传递参数
try{
do_something_in_current_thread();
}
catch(...)
{
t.join(); //关闭子线程
flag = false; //标志位置为关
throw; //还是抛出异常退出
}
t.join(); //正常退出子线程
flag = false; //标志位置为关
当然,使用detach()也行,就是改变一下位置即可。代码如下:
#include <iostream>
#include <thread>
#include <exception>
using namespace std;
void func(bool & flag) // 这里使用引用
{
int i=0;
while(flag)
{
cout << "from func():" << i++ << endl;
_sleep(100);
}
cout << "from func() end!!!" << endl;
}
int main() //主线程
{
bool f = true;
thread t(func, std::ref(f)); //子线程,传递参数
t.detach(); //分离子线程
try{
for(int i = 0; i <50 ; i++)
{
cout << "mian():" << i << endl;
if(i==25) throw std::runtime_error("An error occurred!"); // 手动抛出异常
_sleep(100);
}
}
catch(...)
{
cout<<"something error!!!"<<endl;
f=false;
_sleep(100);
//throw; // 如果要保证程序正常报错的话,建议还是加上这段话
}
f=false;
_sleep(100); // 最后需要等待100ms,不然的话,子线程无法执行end那句话
return 0;
}
可以看到,顺利退出子线程了,但是这么写,如果只是一个的话,还好,如果有多个都可能报错嘞? 那岂不是得写一大堆try ... catch()... ? 这个嘛,目前还在思考有没有解决方法,如果有解决方法的话,后期会补充。
注意3:传递参数使用引用
一般情况下,传递参数很简单,例如:
#include <iostream>
#include <thread>
#include <string>
#include <exception>
using namespace std;
void func(int flag = 10, string s = string())
{
int i=0;
while(flag)
{
cout << "from func():"<<s.c_str() << flag-- << endl;
_sleep(10);
}
cout << "from func() end!!!" << endl;
}
int main() //主线程
{
bool f = true;
int a = 100;
// thread t(func, 100);
thread t(func, a ,"test"); //子线程,传递参数
t.detach(); //分离子线程
for(int i = 0; i <20 ; i++)
{
cout << "mian():" << i << endl;
_sleep(100);
}
cout<<"Main end"<<endl;
return 0;
}
但是如果传递的是引用的话,需要注意,如果传递的是一个函数内部的局部变量,多线程引用了此变量,在函数执行以后,子线程如果再使用此变量容易引发未定义的问题。一般来说,可以使用const &引用模式,然后转为右值以后再传递。比如指针指针unique_ptr,不能复制,只能移动,就需要使用move语句转为右值传递。
使用RAII实现线程管理
使用ThreadGuard类的析构函数来将标志位改变。
#include <iostream>
#include <thread>
#include <exception>
using namespace std;
class ThreadGuard {
std::thread& t;
bool &flag;
public:
//ThreadGuard(std::thread& t_):t(t_) {} // 没有标志位就使用这个
ThreadGuard(std::thread& t_, bool& flag_):t(t_),flag(flag_) {}
~ThreadGuard() {
cout<<"~ThreadGuard()"<<endl;
flag = false;
_sleep(100); // 最后需要等待100ms,不然的话,子线程无法执行end那句话
//if(t.joinable()) // 如果不是detach(),就这么写
// t.join();
}
ThreadGuard(ThreadGuard const&) = delete; //拷贝构造
ThreadGuard& operator=(ThreadGuard const&) = delete; //赋值运算符
};
void func(bool & flag) // 这里使用引用
{
int i=0;
while(flag)
{
cout << "from func():" << i++ << endl;
_sleep(100);
}
cout << "from func() end!!!" << endl;
}
int main() //主线程
{
bool f = true;
thread t(func, std::ref(f)); //子线程,传递参数
t.detach(); //分离子线程
ThreadGuard *tg = new ThreadGuard(t,f);
for(int i = 0; i <50 ; i++)
{
cout << "mian():" << i << endl;
//if(i==25) throw std::runtime_error("An error occurred!"); // 手动抛出异常
_sleep(100);
}
delete tg;
return 0;
}
这里需要注意的是,其实不使用指针,使用变量也可以,不过可以使用大括号来控制,因为离开了大括号的范围,就会自动调用析构函数,例如我这里使用do while的大括号:
#include <iostream>
#include <thread>
#include <exception>
using namespace std;
class ThreadGuard {
std::thread& t;
bool &flag;
public:
//ThreadGuard(std::thread& t_):t(t_) {} // 没有标志位就使用这个
ThreadGuard(std::thread& t_, bool& flag_):t(t_),flag(flag_) {}
~ThreadGuard() {
cout<<"~ThreadGuard()"<<endl;
flag = false;
_sleep(100); // 最后需要等待100ms,不然的话,子线程无法执行end那句话
//if(t.joinable()) // 如果不是detach(),就这么写
// t.join();
}
ThreadGuard(ThreadGuard const&) = delete; //拷贝构造
ThreadGuard& operator=(ThreadGuard const&) = delete; //赋值运算符
};
void func(bool & flag) // 这里使用引用
{
int i=0;
while(flag)
{
cout << "from func():" << i++ << endl;
_sleep(100);
}
cout << "from func() end!!!" << endl;
}
int main() //主线程
{
bool f = true;
thread t(func, std::ref(f)); //子线程,传递参数
t.detach(); //分离子线程
do
{
ThreadGuard tg(t,f);
for(int i = 0; i <50 ; i++)
{
cout << "mian():" << i << endl;
if(i==25) break; // 手动中级退出
_sleep(100);
}
}while(false);
cout<<"Main end"<<endl;
return 0;
}
会发现在中途退出了。所以可见,使用这种方法,可以使用大括号来控制。当然,直接使用大括号也是可以的,不是一定要使用do while的大括号。