文章目录
- 概述
- 不 join 也不 detach
- 执行了detach并不能万事大吉
- 建议使用 join 函数
概述
这里默认你已经了解 std::thread 类的基本使用,和WinAPI多线程编程中 “如何优雅的退出线程” 等相关知识。阅读该文前,建议先看看《多线程 /C++ 11 std::thread 类深入理解和应用实践》 和 《多线程/WinAPI线程退出方式比较分析》这两篇文章。在 函数 join 和 函数 detach 的帮助文档中都讲到,
join(), After a call to this function, the thread object becomes non-joinable and can be destroyed safely.
detach(), After a call to std::thread::detach, the thread object becomes non-joinable and can be destroyed safely.
即在线程对象上调用 join函数或detach函数 后,线程对象便可安全地销毁了,何为安全,真的安全吗?
另外一个问题是,std::thread并未提供,像ExitThread、TerminateThread这样的终止线程的接口,也没有直接提供WaitForSingleObject 类似的等待函数, 当使用 std::thread 进行多线程编程时,似乎只有 “入口函数返回” 这一种方式。另外就是进程退出倒逼线程退出的方式。
不 join 也不 detach
int main()
{
int interval_child = 1, interval_main = 2; //s
//
std::thread t1 = std::thread([&]() {
std::this_thread::sleep_for(std::chrono::seconds(interval_child));
printf("at:%f, t1_entry_func return \n", DalOsTimeSysGetTime());
});
//预留时间,等待次线程结束
std::this_thread::sleep_for(std::chrono::seconds(interval_main));
//
printf("at:%f, main end sleep t1.joinable:%d \n", DalOsTimeSysGetTime(), t1.joinable());
//try {
// t1.join();
// printf("at:%f, t1.join return \n", DalOsTimeSysGetTime());
//}
//catch (const std::exception&) {
// std::cout << "any system_error exception \n";
//}
//system("pause");
return 0;
}
上述代码,运行结果如下:
可以得出,
1、即使线程对象代表的执行线程已经(函数返回)完成,此时对象 t1 依然是 joinable,可加入到其他线程中的。
2、进程退出前,如果没有对 std::thread 对象 t1 执行 join 或 detach 操作,则会触发abort终止进程。
补充,
一般情况下,无法通过异常处理机制捕获导致abort()调用的异常。当调用abort()函数时,理论上,可以通过一些系统相关的底层注册机制拦截或过滤它,我尝试了几种方式都没有成功,这里不再深究。
执行了detach并不能万事大吉
改动detach帮助下的示例程序如下,主要目的在于测试:进程退出后,子线程是否会 继续 完成执行过程。
#include <iostream> // std::cout
#include <thread> // std::thread, std::this_thread::sleep_for
#include <chrono> // std::chrono::seconds
#include <string.h>
#include <fstream>
#include <stdio.h>
#include <windows.h>
using namespace std;
//辅助函数 //写log函数
void WriteLog(const char * format, ...)
{
char buff[128] = { 0 };
va_list ap;
va_start(ap, format);
vsprintf_s(buff, 128, format, ap);
va_end(ap);
//
std::ofstream outfile("result.txt", std::ios_base::app);
outfile.write(buff, strlen(buff));
outfile.close();
}
//辅助函数 //时刻值ms //%f
double DalOsTimeSysGetTime(void)
{
LARGE_INTEGER nFreq; LARGE_INTEGER nBeginTime;
QueryPerformanceFrequency(&nFreq);
QueryPerformanceCounter(&nBeginTime);
return (double)(nBeginTime.QuadPart * 1000 / (double)nFreq.QuadPart);
}
//定义一个C++类对象
class ClassA
{
public:
ClassA(int id): m_id(id) {
WriteLog("CppObject-%d 构造 at %f\n", m_id, DalOsTimeSysGetTime());
//申请堆内存
m_pData = new TData();
}
~ClassA() {
WriteLog("CppObject-%d 析构 at %f\n", m_id, DalOsTimeSysGetTime());
//释放堆内存
if (NULL != m_pData) {
delete m_pData;
m_pData = NULL;
}
}
private:
struct TData { //数据类
int a; int b;
};
TData *m_pData = NULL; int m_id = 0;
};
//入口函数
void pause_thread(int n)
{
//定义在线程栈上的对象
ClassA aObjectInStack(n);
//使得指定的次线程睡眠n秒
std::this_thread::sleep_for(std::chrono::seconds(n));
//
WriteLog("thread_%d pause %d seconds, then return at %f \n", n, n, DalOsTimeSysGetTime());
}
int main()
{
std::ofstream outfile("result.txt", std::ios_base::trunc);
outfile.close(); //清空日志文件
std::cout << "Spawning and detaching 3 threads...\n";
std::thread(pause_thread, 1).detach();
std::thread(pause_thread, 2).detach();
std::thread(pause_thread, 4).detach();
std::cout << "Done spawning threads.\n";
std::cout << "the main thread will now pause for 3 seconds\n";
//you can give the detached threads time to finish (but not guaranteed!):
std::this_thread::sleep_for(std::chrono::seconds(3));
return 0;
}
//CppObject - 1 构造 at 112975428.942300
//CppObject - 2 构造 at 112975430.304200
//CppObject - 4 构造 at 112975431.947500
//thread_1 pause 1 seconds, then return at 112976440.094500
//CppObject - 1 析构 at 112976441.718200
//thread_2 pause 2 seconds, then return at 112977441.448000
//CppObject - 2 析构 at 112977442.140000
//id==4的线程 入口函数未完成执行,其内的对象未触发析构。
可以看出来,在进程退出后,
1、正在运行的线程入口函数的执行是"戛然而止"的,如果此时pause_thread入口函数内是while循环模式的,则线程将极有可能要死在while单次循环执行过程中,这是危险不优雅的。
2、入口函数若没有执行返回,则不会触发线程函数内对象的析构过程。
3、在 detach 作用下,并不会保证入口函数返回。 进程退出时,如果入口函数已经完成,则没有任何问题。但如果此时入口函数尚在执行过程中(如等待、耗时IO操作等),将与windowsAPI::ExitThread 的使用效果如出一辙。故,在使用detach分离线程后,若不加以控制使得入口函数能保证是可返回的,虽然系统会释放线程栈,但是由于此时析构过程未触发,依然存在m_pData堆内存泄漏的问题。
进程退出,
只要进程不退出(如将主线程使用system(“pause”) 或者 使用while循环睡眠,来保持运行),则detach的次线程便可以一直运行下去(如果它能一直运行下去),这是毋庸置疑的。如果宿主线程(或称MSDN中的calling thread调用线程)不是主线程,而是其他次线程,则线程对象在被detach后更不会退出执行。
建议使用 join 函数
讨论来讨论去,还是建议使用 join函数老实的等待执行线程退出。为此,我们可能需要将线程对象创建为成员变量或全局变量,以能在线程停止函数中调用join函数,实现等待操作。如下:
//在stop函数中利用join等待
void MyThreadStop() {
m_runningFlag = fale;
join();
}
平日里我们见到的示例程序大都简单到只是在main函数中创建子线程并直接退出,在main函数中 “同步地” 调用detach或join就完犊了。而实际使用中通常会更 “异步” 一些。如,在Qt环境的事件循环机制下,join的 “异步” 调用可能是:
#mian.cpp
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MianWnd w;
w.show();
return a.exec();
}
#mianWnd.h
class MianWnd : public QMainWindow
{
Q_OBJECT
public:
MianWnd(QWidget *parent = Q_NULLPTR);
~MianWnd();
private:
//函数入口
void pause_thread(int n);
//停止函数
void MyThreadStop();
private:
//线程
std::thread m_thread;
//运行标志
bool m_runningFlag = true;
//堆栈资源
struct TStruct
{ int a; int b; } *m_pResource;
private:
Ui::MianWndClass ui;
};
#mainWnd.cpp
//辅助函数 //时刻值ms
double DalOsTimeSysGetTime(void)
{
LARGE_INTEGER nFreq; LARGE_INTEGER nBeginTime;
QueryPerformanceFrequency(&nFreq);
QueryPerformanceCounter(&nBeginTime);
return (double)(nBeginTime.QuadPart * 1000 / (double)nFreq.QuadPart);
}
//辅助函数
void TraceForVStudio(char *fmt, ...)
{
char out[1024] = { 0 };
va_list body;
va_start(body, fmt);
vsprintf_s(out, 1024, fmt, body);
va_end(body);
OutputDebugStringA(out);
OutputDebugStringA("\r\n");
}
//入口函数
void MianWnd::pause_thread(int n)
{
while (m_runningFlag) //
{
if (NULL != m_pResource) //子线程内使用(共享)资源
TraceForVStudio("Using Resource# a:%d b:%d ", ++m_pResource->a, ++m_pResource->b);
std::this_thread::sleep_for(std::chrono::seconds(n));
}
//may do something other..
}
//停止函数
void MianWnd::MyThreadStop()
{
m_runningFlag = false;
m_thread.join(); //block
}
//构造函数
MianWnd::MianWnd(QWidget *parent)
: QMainWindow(parent)
{
ui.setupUi(this);
//资源
m_pResource = new TStruct();
//线程
m_thread = std::thread(&MianWnd::pause_thread, this, 5);
}
//析构函数
MianWnd::~MianWnd()
{
//停止线程
TraceForVStudio("Wait Begin At:%f", DalOsTimeSysGetTime()) ;
MyThreadStop();
TraceForVStudio("Wait Finish At:%f", DalOsTimeSysGetTime());
//销毁线程资源
if (NULL != m_pResource)
{ delete m_pResource; m_pResource = nullptr; }
//销毁UI及其子窗口对象..
}
//关闭窗口触发析构过程
//Using Resource# a:1 b:1
//Using Resource# a:2 b:2
//...
//Wait Begin At : 93813551.843300
//Exit Thread At : 93816981.917300 //about 3.5s
//Wait Finish At : 93816988.057200 //about 007ms
通过上述测试,可以确定join函数可以起到很好的等待线程退出的效果,比std::condition_variable 方便的多。上述,每5s完成单次循环,我随机关闭窗口触发析构过程,m_runningFlag 置零后大约过了3.5s后生效,然后入口函数退出,又过了7ms左右,join函数从阻塞过程中返回,析构过程继续执行堆栈资源的销毁过程。