多线程/WinAPI线程退出方式比较分析

文章目录

  • 概述
  • ExitThread
  • TerminateThread
  • 进程退出逼迫线程退出?
  • 线程入口函数返回
  • 验证无法执行C++对象析构
  • 不同方案的比较
  • 参考或关联

概述

关于如何终止 Windows 线程,在 MSDN Terminating a Thread 一文中列举的很详细:
A thread executes until one of the following events occurs:

  1. The thread calls the ExitThread function. 调用 WINAPI ExitThread 线程退出函数。
  2. Any thread of the process calls the ExitProcess function. 调用 WINAPI ExitProcess 进程退出函数。
  3. The thread function returns. 使得线程入口函数返回。
  4. Any thread calls the TerminateThread function with a handle to the thread. 使用 TerminateThread 粗暴结束线程。
  5. Any thread calls the TerminateProcess function with a handle to the process. 使用 TerminateProcess 粗暴结束进程。

本文将对比如上几种Windows线程退出方式的主要特点,并对其中涉及到的某些知识点做延伸分析。后续章节将通过Demo来验证其中的一些说法。Terminating a thread has the following results:

  • Any resources owned by the thread, such as windows and hooks, are freed.
  • The thread exit code is set.
  • The thread object is signaled.
  • If the thread is the only active thread in the process, the process is terminated.

ExitThread

//note 本函数没有线程句柄参数 /只能用以关闭调用它的线程
VOID WINAPI ExitThread( _In_ DWORD dwExitCode ); 
//dwExitCode [in] 
The exit code for the thread.
//Return value
This function does not return a value.

ExitThread is the preferred method of exiting a thread in C code. However, in C++ code, the thread is exited before any destructors can be called or any other automatic cleanup can be performed. Therefore, in C++ code, you should return from your thread function.
该函数是C代码中退出线程的首选方案。但在C++代码中,线程会在任意析构函数或其他清理过程执行前退出,因此,在C++编程中,最好使用入口函数返回的方式,而不是此函数来退出线程。(后续章节会有代码验证)

关闭一个线程将主要产生如下结果,
When this function is called (either explicitly or by returning from a thread procedure), the current thread’s stack is deallocated, all pending I/O initiated by the thread is canceled, and the thread terminates. The entry-point function of all attached dynamic-link libraries (DLLs) is invoked with a value indicating that the thread is detaching from the DLL. If the thread is the last thread in the process when this function is called, the thread’s process is also terminated.
当调用ExitThread函数(无论是显式调用还是从线程入口函数返回)时,当前线程的堆栈被释放,由该线程发起的所有未完成的I/O将被取消,线程将终止。所有已附加的动态链接库的入口函数(DllMain)都将被调用,并传递指示该线程正在从DLL中分离的值。(参见DllMain函数说明或《动态库/DLL的入口函数说明》可知,此时会在其第二个参数上传递 DLL_THREAD_DETACH 枚举值,此处不深究)。
所谓 “附加到线程的动态库” 是一种在多线程编程时可以使用的技术。DLL包含了可重用的代码等资源,可在应用程序中动态地加载和卸载。动态库可被附加到单个线程而不是整个进程,这种技术被称为 “线程本地存储TLS” 或 “线程特定数据TSD”。线程本地存储允许线程在运行过程中访问全局变量或静态变量的本地副本,从而避免线程之间互相干扰。每个线程可以在需要时分配其自己的局部内存,这样就可以保留它的状态,附加到线程的 DLL 在每个线程的内存空间中保留一个独立的拷贝。在次技术下每个线程可以独立地使用 DLL 资源,而无需与其它线程共享它们,其使得线程间通信更加方便,也提高了多线程编程的效率和可靠性。

The state of the thread object becomes signaled, releasing any other threads that had been waiting for the thread to terminate. The thread’s termination status changes from STILL_ACTIVE to the value of the dwExitCode parameter. 线程对象的状态被设置为 signaled(有信号),进而系统将释放所有已等待该线程终止的其他线程。线程的终止状态从 STILL_ACTIVE 更改为 dwExitCode 参数的值。
Terminating a thread does not necessarily remove the thread object from the operating system. A thread object is deleted when the last handle to the thread is closed.

线程的创建、退出过程等在进程内是串行的,
The ExitProcess, ExitThread, CreateThread, CreateRemoteThread functions, and a process that is starting (as the result of a CreateProcess call) are serialized between each other within a process. Only one of these events can happen in an address space at a time. (延伸:地址空间是属于进程的,每个进程都有自己独立的地址空间。在进程内部创建的线程共享该进程的地址空间,也就是说它们执行代码、访问内存都使用进程的地址空间。线程之间可以共享数据,而这些数据是存储在进程的地址空间中的共享内存区域。)
This means the following restrictions hold: 意味着有如下限制:
During process startup and DLL initialization routines, new threads can be created, but they do not begin execution until DLL initialization is done for the process. 在进程启动或动态库初始化过程中,可以创建线程,但是它必须等到DLL初始化完成后才会执行。
Only one thread in a process can be in a DLL initialization or detach routine at a time. 同一时刻,进程内只能有一个线程处于初始化或者分离过程。
ExitProcess does not return until no threads are in their DLL initialization or detach routines. 除非是进程内全部的线程都结束了初始化或分离过程,否则进程不会退出。

当DLL或EXE链接到静态CRT库时,不建议使用ExitThread 函数,
MSDN中的这一小节主要是描述,如果一个可执行文件或动态库中使用了静态CRT库(an executable that is linked to the static CRT 或 a DLL if the DLL is linked to the static CRT ),则对于此类EXE或DLL中的线程,其对 ExitThread 函数的调用是不安全不可靠的。

A thread in an executable(常规意义上它是形容词"可执行的",但网络释意可为"可执行文件") that is linked to the static C run-time library (CRT) should use _beginthread and _endthread for thread management rather than CreateThread and ExitThread. Failure to do so(未能做到的话) results in small memory leaks when the thread calls ExitThread. Another work around is to link the executable to the CRT in a DLL instead of the static CRT. 另一个解决办法是使得可执行文件链接到动态CRT库,而不是静态CRT库。

下边这句表述潜台词太多,并不好理解。上一段描述的是 A thread in an executable 相关情况,下面这段段描述的是 a thread in a DLL 相关情况。
Note that this memory leak only occurs from a DLL if the DLL is linked to the static CRT and a thread calls the DisableThreadLibraryCalls function. Otherwise, it is safe to call CreateThread and ExitThread from a thread in a DLL that links to the static CRT.
其中提到的DisableThreadLibraryCalls 函数,原型如下,它实现了 “禁用指定DLL 的 DLL_THREAD_ATTACH 和 DLL_THREAD_DETACH 通知” 的功能,以减小某些程序的工作集大小。调用此函数,则 DLL 将不会收到与线程库相关的通知。另外我猜测是这样的,EXE没有DllMain函数,与DLL关闭DllMain函数接收相关通知,在效果上是一致的。

BOOL WINAPI DisableThreadLibraryCalls(__in HMODULE hModule);
//HMODULE hModule,将要被禁用DLL_THREAD_ATTACH和DLL_THREAD_DETACH通知的dll的模块句柄。

有了上述补充后,再来翻译最后这段英文帮助:要注意的是,DLL中的线程只有在如下情况中调用 ExitThread 函数才会导致内存泄漏:DLL使用了静态CRT库,且 DisableThreadLibraryCalls 函数被调用执行过。在此情况下,如果从 DLL 中的线程中调用 ExitThread 函数,则会导致静态 CRT 库在堆中保留线程特定数据,从而导致内存泄漏。只要未启用 DisableThreadLibraryCalls ,从 DLL 中的线程中调用 CreateThread 和 ExitThread 函数是安全的,并且不会导致内存泄漏问题。
关于 "上述提到的小内存泄漏具体是如何发生的"等相关问题,与本主题关系不大,请参见其他文章。

TerminateThread

BOOL WINAPI TerminateThread( _Inout_ HANDLE hThread, _In_ DWORD  dwExitCode);
//hThread [in, out] 
//A handle to the thread to be terminated. The handle must have the THREAD_TERMINATE access right. For more information, see Thread //Security and Access Rights.
//dwExitCode [in] 
//The exit code for the thread. Use the GetExitCodeThread function to retrieve a thread's exit value.
//Return value
//If the function succeeds, the return value is nonzero. If the function fails, the return value is zero. To get extended error //information, call GetLastError.

TerminateThread is used to cause a thread to exit. When this occurs, the target thread has no chance to execute any user-mode code. DLLs attached to the thread are not notified that the thread is terminating. The system frees the thread’s initial stack. Windows Server 2003 and Windows XP:The target thread’s initial stack is not freed, causing a resource leak.
相比 ExitProcess 退出线程的过程,TerminateThread 要更加的糟糕。调用此函数时,被关闭的目标线程,将没有机会去执行任何的用户代码。附加到目标线程的动态库,也不会得到相关的线程退出通知。最终,只能由操作系统来释放线程的申请的栈空间。

TerminateThread is a dangerous function that should only be used in the most extreme cases. You should call TerminateThread only if you know exactly what the target thread is doing, and you control all of the code that the target thread could possibly be running at the time of the termination. For example, TerminateThread can result in the following problems:
在极端的情况下,你可以使用 TerminateThread 函数。这时候,你要确定你是准确知道目标线程的工作细节的,并且你能完全控制相关代码。TerminateThread 可能导致如下不良后果:

  • If the target thread owns a critical section, the critical section will not be released.
  • If the target thread is allocating memory from the heap, the heap lock will not be released.
  • If the target thread is executing certain kernel32 calls when it is terminated, the kernel32 state for the thread’s process could be inconsistent. 如果目标线程在被终止时正在执行某些 kernel32 调用,则该线程所在进程的 kernel32 状态可能(与实际)不一致。
  • If the target thread is manipulating 操纵 the global state of a shared DLL, the state of the DLL could be destroyed, affecting other users of the DLL.

A thread cannot protect itself against TerminateThread, other than by controlling access to its handles. The thread handle returned by the CreateThread and CreateProcess functions has THREAD_TERMINATE access, so any caller holding one of these handles can terminate your thread.
线程无法自我保护免受 TerminateThread 的影响,除非通过控制对其句柄的访问来实现。

If the target thread is the last thread of a process when this function is called, the thread’s process is also terminated. The state of the thread object becomes signaled, releasing any other threads that had been waiting for the thread to terminate. The thread’s termination status changes from STILL_ACTIVE to the value of the dwExitCode parameter.

线程对象自身的释放
在CreateThread的MSDN中提到,The thread object remains in the system until the thread has terminated and all handles to it have been closed through a call to CloseHandle。
在WINAPI ExitThread 和 WINAPI TerminateThread 的MSDN中也有提及到,Terminating a thread does not necessarily remove the thread object from the operating system. A thread object is deleted when the last handle to the thread is closed。
即线程对象自身不会因为调用线程终止函数而被释放,而是需要调用CloseHandle关闭线程句柄。

进程退出逼迫线程退出?

这里主要关注,如果进程退出前没有显式的退出进程内的全部线程,那么待到进程退出时,其线程们将怎么退场呢?是默认执行了 ExitThread 过程吗?还是使用了什么其他更狠的手段!

在 MSDN Terminating a Process 章节中,有如下和线程退出相关的表述:
Do not terminate a process unless its threads are in known states. If a thread is waiting on a kernel object, it will not be terminated until the wait has completed. This can cause the application to stop responding. 不要在进程内线程的状态未知的情况下去终止一个进程。如果进程内的一个线程正在等待内核对象,那么在此等待结束前,进程无法退出。这将导致应用程序无法响应的情况。
The ExitProcess function does not return until there are no threads are in their DLL initialization or detach routines.

If a process is terminated by TerminateProcess, all threads of the process are terminated immediately with no chance to run additional code. This means that the thread does not execute code in termination handler blocks. 如果对进程应用了 TerminateProcess 操作,其效果相当于对其下的全部线程应用了 TerminateThread 操作。

在如何退出进程的方法描述中,
1、Any thread of the process calls the ExitProcess function. Note that some implementation of the C run-time library (CRT) call ExitProcess if the primary thread of the process returns.
2、The last thread of the process terminates.
3、Any thread calls the TerminateProcess function with a handle to the process.
4、For console processes, the default console control handler calls ExitProcess when the console receives a CTRL+C or CTRL+BREAK signal.
5、The user shuts down the system or logs off.

MSDN ExitProcess function 中提到 Exiting a process causes the following:
1、All of the threads in the process, except the calling thread, terminate their execution without receiving a DLL_THREAD_DETACH notification.
2、The states of all of the threads terminated in step 1 become signaled.
3、The entry-point functions of all loaded dynamic-link libraries (DLLs) are called with DLL_PROCESS_DETACH.
4、After all attached DLLs have executed any process termination code, the ExitProcess function terminates the current process, including the calling thread.
5、The state of the calling thread becomes signaled.
6、All of the object handles opened by the process are closed.
7、The termination status of the process changes from STILL_ACTIVE to the exit value of the process.
8、The state of the process object becomes signaled, satisfying any threads that had been waiting for the process to terminate.

其中第1条讲到,在 ExitProcess 调用后,除了调用 ExitProcess 的那个线程,所有属于该进程的线程都被终止,且DLL入口函数不会收到相关通知。这有点类似TerminateThread函数的行为。因此使用进程退出逼迫线程退出是及其不负责的做法。

线程入口函数返回

始终都应该将线程设计成这样的形式,即当想要线程终止运行时,它们就能够返回。这是确保所有线程资源被正确地清除的唯一办法。如果线程能够返回,就可以确保下列事项的实现:
  • 在线程函数中创建的所有 C++ 对象均将通过它们的撤消函数正确地撤消。(下文会有测试)
  • 操作系统将正确地释放线程堆栈使用的内存。
  • 系统将线程的退出代码(在线程的内核对象中维护)设置为线程函数的返回值。
  • 系统将递减线程内核对象的使用计数。

验证无法执行C++对象析构

写一个Demo,以验证使用 ExitThread 执行线程退出时,确实不会执行 C++对象的析构过程 。主要思路:在子线程入口函数内创建类A的局部变量(定义在线程栈空间上),使用不同的方式退出该子线程,观察类A析构函数的执行情况。

#include
#include

//控制子线程以"入口函数返回"形式推出
static bool s_bThreadExitByEnterFuncReturn = true;
//控制子线程以"ExitThread调用"形式推出
static bool m_bThreadExitByExitThreadFunc = false;

//数据类
struct TData
{
    int a;  int b;
};

//定义一个C++类对象
class ClassA
{
public:
    ClassA()
    {
        printf("ClassA 构造函数\n");
        //申请堆内存
        m_pData = new TData();
    }
    ~ClassA()
    {
        printf("ClassA 析构函数\n");
        //释放堆内存
        if (NULL != m_pData)
        {
            delete m_pData;
            m_pData = NULL;
        }
    }
private:
    TData *m_pData = NULL;
};


DWORD WINAPI ThreadFunc(LPVOID p)
{
    //定义在线程栈上的对象
    ClassA aObjectInStack;

    while (s_bThreadExitByEnterFuncReturn)
    {
        //
        Sleep(1000);
        //
        printf("子线程 pid = %d\n", GetCurrentThreadId());

        if (m_bThreadExitByExitThreadFunc)
        {
            ExitThread(0);  //同步退出线程
        }
    }

    printf("线程入口函数返回!\n");  //ExitThread时是无法执行此代码的
    
    return 0;
}

void main()
{
    HANDLE hThread; DWORD  threadId;
    //
    hThread = CreateThread(NULL, 0, ThreadFunc, 0, 0, &threadId);

    while (true)
    {
        printf("主线程 pid = %d\n", GetCurrentThreadId());
        Sleep(2000);
#if 0
        //以ExitThread方式退出线程
        m_bThreadExitByExitThreadFunc = true;
#else
        //以入口函数返回方式退出线程
        s_bThreadExitByEnterFuncReturn = false;
#endif
    }
}
以 ExitThread方式退出线程 以入口函数返回方式退出线程
在这里插入图片描述 多线程/WinAPI线程退出方式比较分析_第1张图片

经上述测试可以得出,即使是定义在线程内的局部变量(线程栈空间上的对象),在使用 ExitThread 退出线程时,也不会触发被定义对象的析构函数执行。如果线程内C++对象是创建在堆上,使用 ExitThread 操作时,根本没法执行手动的delete操作,更是会造成异常。要注意的是,像上述示例中的情况,其内存泄漏,并不是指 aObjectInStack 所占用的栈内存的泄漏,而是由于ClassA无法执行析构过程,而导致对象的 m_pData 数据内存无法释放。

不同方案的比较

ExitThread 函数将终止线程的运行,并促使操作系统清除其使用的所有系统资源。ExitThread被认作是一种非正常的线程终止操作,主要是因为使用他退出线程前,无法触发任何C++对象析构函数的执行,可能造成内存泄漏。
ExitThread 总是撤消它的调用线程(它没有句柄参数),而 TerminateThread 能够撤消任何线程(它可以指定线程句柄hThread )。但要注意 TerminateThread 函数是异步运行的函数,它告诉系统你想要线程终止运行,但当函数返回时并不能保证线程已被撤消。如需确切地知道该线程是否已经终止运行,必须传递线程句柄来调用 WaitForSingleObject 或者类似的函数。

If a thread is terminated by ExitThread, the system calls the entry-point function of each attached DLL with a value indicating that the thread is detaching from the DLL (unless you call the DisableThreadLibraryCalls function). If a thread is terminated by ExitProcess, the DLL entry-point functions are invoked once, to indicate that the process is detaching. DLLs are not notified when a thread is terminated by TerminateThread or TerminateProcess.
当使用ExitThread终止线程运行时,系统会调用DLL入口函数来通知它线程分离事件(除非DisableThreadLibraryCalls 被调用了)。如果用 TerminateThread 强迫线程终止,DLL 就不接收此通知。

The TerminateThread and TerminateProcess functions should be used only in extreme circumstances, since they do not allow threads to clean up, do not notify attached DLLs, and do not free the initial stack.
当使用线程入口函数返回方式或调用 ExitThread 的方法撤消线程时,该线程的内存堆栈也被撤消。如果使用 TerminateThread,那么在拥有线程的进程终止运行之前,系统不撤消该线程的堆栈,这是 Microsoft 有意为之。因为如果其它正在执行的线程要引用 TerminateThread 强制撤消的线程堆栈上的值,会出现访问冲突的问题。如果将已经强制撤消的线程的堆栈留在内存中,那么其它线程就可以继续很好地运行。

参考或关联

《 MSDN Terminating a Thread 》
《 MSDN Terminating a Process 》
《 MSDN ExitThread function》
《 MSDN TerminateThread function》
《 MSDN ExitProcess function》
《多线程/std::thread线程退出方式详解》
《多线程/必须等待线程退出/优雅且安全的退出线程执行》

你可能感兴趣的:(多线程编程,windows多线程编程,ExitThread)