多线程/等待WinAPI或std::thread线程执行的退出

概述

大约3年前,我写下此篇文章的一小段草稿,给自己留下了几个问题,尘封至此。
1、怎么算是优雅的退出线程?
2、线程被阻塞的时候怎么退出线程?
3、线程可能依赖堆数据、设备等资源,保险起见,只要使用它们便要先判断其状态。但是在一些极端的情况下,如,就在你验证资源有效的那一刻,资源被干掉了,此时你的线程处理过程便极有可能产生异常。所以我总感觉,在大多时候,如果能先100%确认线程已停转,再去销毁其使用到的全部资源,似乎更靠谱!

其实只要优雅了,自然就更安全了。在线程退出时,可能常常关注如下几方面问题:
0、线程的停止过程应该是可控的,不可忽略掉的!
1、销毁线程依赖的(共享)资源(如堆栈)前,最好先确保线程已退出?
2、如果线程过程存在挂起的可能,则退出线程前要先唤醒线程?(耗时操作致使while-Flag延时生效)
3、线程退出的时刻,要保证入口函数是已经返回(执行完成)的,才算优雅吧?
4、若不等待线程 “真真正正” 完完全全的退出,会带来哪些灾难?
5、线程退出时,在哪些情况下,会造成内存泄漏等问题,如何避免?
6、进程退出后,未结束的线程能继续存活多久?会发生什么?

何为优雅地退出?

结合 《多线程/WinAPI线程退出方式详解》、《多线程/std::thread线程退出方式详解》、《多线程 /C++ 11 std::thread 类深入理解和应用实践》几篇文章中的相关表述,可知所谓的优雅大抵如是:无论如何,使得线程以 “入口函数完成返回” 的形式结束是最友好的线程结束方式,这也是确保所有线程资源被正确地清除的唯一办法。如果线程入口函数能够return返回,就可以确保下列事项的实现:

  • 在线程函数中创建的所有 C++ 对象均将通过它们的撤消函数正确地撤消。
  • 操作系统将正确地释放线程堆栈使用的内存。
  • 系统将线程的退出代码(在线程的内核对象中维护)设置为线程函数的返回值。
  • 系统将递减线程内核对象的使用计数。

一个好的设计,应该始终使得线程以 “入口函数完成返回” 的形式结束。但函数的执行是需要时间或需要条件的,如果某一时刻线程恰好工作处于,睡眠、读写IO操作、bool运行标志、队列阻塞、条件变量阻塞等等的状态,线程将无法退出或无法立即退出。因此,如何保证线程入口函数总是能以函数返回的形式结束,才是保证友好退出线程的重中之重。

为什么要等待线程结束?

为了更好的解释这个问题,我用伪代码写了个入口函数如下,

void work_thread(int n)
{
    //申请堆内存
    char *bufferHeap = (char *)malloc(256);
    //定义在线程栈上的对象/需要在析构中释放堆空间
    ClassB aObjectInStack(n);  
    
	//线程循环
    while (m_bRunFlag)  {
        do_work1();  //较耗时
        do_work2();  //可能因等待某种条件而阻塞
        do_work3();
    }

    //释放堆数据
    free(bufferHeap);
    //释放系统资源
    release_os_rc1();
    //日志记录
    WriteLog(...);
}

通常,我们使用bool类型的运行标志,以控制结束线程入口函数内的while循环,使得线程入口函数返回的形式退出。

置零 runFlag 后,while 可能无法立即感知到,
while循环体每次执行都是需要时间的,我们不妨假设循序体内 do_work1函数 每次执行要耗时3s钟,那么当我们将runFlag置false后,最糟糕的情况下,是2.9999s后当while下次循环开始时它才能生效。也是在这个2.9999s的间隙中,陷阱就出来了:如果不等待入口函数返回,由于置位runFlag的ThreadStop函数是同步立即返回的,这对ThreadStop函数的调用者来说是一种欺骗。这种欺骗将带来诸多问题,如下列举两点:

进程抢先退出,
在上述2.9999s的间隙中,由于没有等待线程退出的机制,会有进程迫使线程退出的情况发生,该过程是粗暴的。具体的可以参见上文提到的另外几篇文章,此时线程的执行过程可能是 “戛然而止” 的,不受控制的。如,进程待退出的那一刻,线程可能执行到do_work1的某行代码、或do_work3的某行代码,由于进程迫使线程退出,后续代码将没有机会执行。这样,入口函数便不能正常返回,while 循环外的释放操作也不会被执行,相关类对象如aObjectInStack的析构函数也不会被触发。这就会造成,资源异常占用,内存泄漏等问题。

线程使用的资源已经被释放,
客户端在ThreadStop函数返回后, “大胆地” 将线程Process依赖的对象都释放掉。但线程Process此时并未真停止,当其运行到资源操作相关代码行的时候,就可能访问野指针、无效数据、无效资源等,造成异常;或者在你判空操作或其他异常检查代码的作用下,你的代码进入到异常处理,如果你没有严谨的异常处理过程,此时程序还是可能会出现不期望行为或者产生不被期望的脏数据。因此我的习惯通常是,如果可以,则尽量的在保证使用资源的线程真实退出了,再销毁相关资源。

使用条件变量等待线程退出

这是我最早想到的方案,尽管没过多久我就否定了该方案,强迫症还是使我实现并测试了该方案的效果。而且还在这个方案上爬了半天的坑,加深了我对std::condition_variable类wait、notify函数的理解。这个方案是适用于任何形式的线程入口函数的,部分主要代码如下:

//定义私有类(用以等待线程退出)
class ITaskComm_Private
{
public:
    typedef struct tagWaitThreadExit
    {
        std::mutex *p_mutex;  //互斥锁
        std::condition_variable *p_condition_variable; //条件变量 
    } TWaitThreadExit;

public:
    ITaskComm_Private(int imaxCount)
    {
        m_iMaxOfCount = imaxCount;
        m_arrayWaitCondition = (TWaitThreadExit *)malloc(sizeof(TWaitThreadExit) * imaxCount);
        for (int index = 0; index < imaxCount; index++)
        {
            m_arrayWaitCondition[index].p_mutex = new std::mutex();
            m_arrayWaitCondition[index].p_condition_variable = new std::condition_variable();
        }
    }
    ~ITaskComm_Private()
    {
        for (int index = 0; index < m_iMaxOfCount; index++)
        {
            delete m_arrayWaitCondition[index].p_mutex;
            m_arrayWaitCondition[index].p_mutex = NULL;
            delete m_arrayWaitCondition[index].p_condition_variable;
            m_arrayWaitCondition[index].p_condition_variable = NULL;
        }
        free(m_arrayWaitCondition);
        m_arrayWaitCondition = NULL;
    }
public:
    TWaitThreadExit *WaitCondition(int iDesID)
    {
        assert((0 != iDesID) && (iDesID <= m_iMaxOfCount));
        return &m_arrayWaitCondition[iDesID - 1];
    }

private:
    TWaitThreadExit *m_arrayWaitCondition;
    int m_iMaxOfCount;
};

//自定义线程管理基类
class BDSVR_API_EXPORT ITaskComm
{ ....
public:
    //确认线程已退出 
    bool i_task_wait(int iLockID)   //ID from 1
	{
	    std::condition_variable *pCondition = m_TaskCommPrivate->WaitCondition(iLockID)->p_condition_variable;
	   //等待线程退出
	   std::unique_lock<std::mutex> lck_unique(*m_TaskCommPrivate->WaitCondition(iLockID)->p_mutex);
	   //@note 
	   if (std::cv_status::timeout == pCondition->wait_for(lck_unique, std::chrono::milliseconds(8000)))
	   {
	       AflDebugError("Task:%s LockID:%d wait 8s Exit Failure", m_TaskCommPrivate->m_strTaskName.c_str(), iLockID);
	       return false;
	   }
	   return true;
	}
    //确认线程已退出 
    void i_task_notify(int iLockID) //ID from 1
    {
	    //illu_1 填坑 //预留时间以确保条件变量先进入等待状态 
	    std::this_thread::sleep_for(std::chrono::milliseconds(100));
	    //
	    std::condition_variable *pCondition = m_TaskCommPrivate->WaitCondition(iLockID)->p_condition_variable;
	    //
	    pCondition->notify_all();
	}

如上基础代码,在从ITaskComm派生的线程类中:我们在TaskStop函数中,将线程循环的runningFlage置零后,调用i_task_wait等待线程退出;在线程入口函数的while循环之外调用i_task_notify函数,结束TaskStop函数的等待。当时踩了这样一个坑:
我有一个数据流接收处理线程,使用的是socket阻塞模型,当没有数据接收时,recv函数是挂起的。为了能在TaskStop函数中,置零操作后使得标志生效,我当时采用了关闭套接字的方法以触发recv函数打断阻塞返回ERROR。在实际调试中发现,线程已经明确的退出了(notify过程确认被执行了),但是TaskStop下的wait_for过程却没有收到通知,直至std::cv_status::timeout超时。
一开始还怀疑自己ITaskComm_Private的类构造有问题,前后鼓捣了两三个小时。中午在一篇资料中发现一句话"notify_all函数唤醒所有阻塞线程,若无线程等待则该函数不执行任何操作"的启发。TaskStop函数中,我在调用i_task_wait函数前,执行套接字关闭操作,可能使得线程在极短的时间内退出,也就是说,在关闭套接字的"瞬间" – notify_all就可能被执行了,而此时wait_for过程还没有被调用,尤其是在wait_for前还有调试打印信息的时候。
为了使得这种确认线程已退出的方案生效,增加了上文中"illu_1 填坑"处的补正代码。肯定有更好的修补方案,未再深入。

白惊喜一场的方案,
在查看condition_variable头文件帮助文档时,发现一个与thread相关的函数,以为发现了新大陆,结果虚惊一场。

void notify_all_at_thread_exit (condition_variable& cond, unique_lock<mutex> lck);

When the calling thread exits, all threads waiting on cond are notified to resume execution.

更简洁地等待std::thread执行线程退出

曾经还因为std::thread中没有wait类似的函数而抱怨,后来才知道是自己没理解透彻。当慢慢整理完《多线程 /C++ 11 std::thread 接口和属性详解 /std::thread 应用实践》中关于joinable属性的那一小节后,很容易的就联想出如下新方案,其主要理论支持:std::thread::detach函数 与 std::thread::join函数不是用来启动线程入口函数的,因为std线程对象,创建即启动;它们的本意更可能是为线程退出提供方式方法。

//在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函数从阻塞过程中返回,析构过程继续执行堆栈资源的销毁过程。

WinAPI中等待线程退出的方式

DWORD WaitForSingleObject([in] HANDLE hHandle,  [in] DWORD  dwMilliseconds);
//[in] hHandle 对象的句柄。可以是:控制台输入、事件、内存资源通知、Mutex、进程、Semaphore、线程、可等待计时器。
//[in] dwMilliseconds 超时间隔(以毫秒为单位)。如果 dwMilliseconds 为 INFINITE,则仅当发出对象信号时,该函数才会返回。

在windows上,通常用 WaitForSingleObject 函数,等待一个线程的退出。除此之外,还可以使用WaitForMultipleObjects、WaitForThreadpoolWorkCallbacks、MsgWaitForMultipleObjects 等API函数,此处不赘述。

#include <iostream>
#include <windows.h>

DWORD WINAPI ThreadProc(LPVOID lpParam)
{
    for (int i = 0; i < 3; ++i) {
        printf("at:%f, Thread is running %d \n", DalOsTimeSysGetTime(), i);
        Sleep(1000);
    }

    return 0;
}

int main()
{
    // 创建线程
    HANDLE hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
    if (hThread == NULL) {
        std::cerr << "Failed to create thread." << std::endl;
        return 1;
    }

    // 等待线程退出
    printf("at:%f, wait thread exit begin \n", DalOsTimeSysGetTime());
    DWORD dwResult = WaitForSingleObject(hThread, INFINITE);
    printf("at:%f, wait thread exit finish \n", DalOsTimeSysGetTime());

    if (dwResult == WAIT_OBJECT_0) {
        std::cout << "Thread exited successfully." << std::endl;
    }
    else {
        std::cerr << "Failed to wait for thread exit." << std::endl;
    }

    // 关闭线程句柄
    CloseHandle(hThread);

    return 0;
}

使用 std::thread 进行C++多线程编程时,使用detach分离线程, 此时可以借助 native_handle 接口获得特定实现下的本地线程句柄,然后使用Windows API 的接口,来实现对线程结束的等待。

		//方式1
        //t1.join();
        //printf("at:%f, t1.join wait thread_func return \n", DalOsTimeSysGetTime());
        
        //方式2
		t1.detach();
        void *H = t1.native_handle();
        DWORD dr = WaitForSingleObject(H, INFINITE);
        printf("at:%f, Windows wait thread_func return \n", DalOsTimeSysGetTime());

其他注意事项

我们最好在完全确认线程已停止运行后,再去销毁线程过程使用的对象,这样不容易出问题,也可以避免在线程执行过程中频繁的去做NULL指针判断,从而在一定程度上提上线程执行效率。
其他注意:
执行delete资源对象操作后,被释放的指针一定要做赋值NULL操作,这并不是空穴来风。否则别处的线程/逻辑可能会再次判断它不为空,并对其再进行释放操作,这也可能导致程序崩溃。
其他注意:
以WinAP多I线程编程为例,如果线程被阻塞挂起了,此时使用ExitThread、等函数,是可以打断线程执行的,但这是粗鲁的方法。合理的方式时,给予合理的刺激,打断阻塞过程,使得入口函数自然返回。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值