本系列意在记录Windwos线程的相关知识点,包括线程基础、线程调度、线程同步、TLS、线程池等。
进程与线程
理解线程是至关重要的,每个进程至少有一个线程,进程是线程的容器,线程才是真正的执行体,线程必然在某个进程的上下文中运行。进程拥有惰性,如果进程中所有的线程都已结束,那么进程也就没有存在的必要了。
一个进程由如下两部分组成:1、一个进程地址空间;2、一个进程内核对象
一个线程由如下两部分组成:1、一个线程栈;2、一个线程内核对象
线程的开销要比进程少很多,所以在解决编程问题的时候尽量考虑在当前进程中创建线程而不是创建新的进程。然而,线程的切换需要消耗一定数量的CPU资源,因此,也不是说可以毫无顾忌的使用线程来处理问题。
线程生命周期
线程的创建
系统创建一个线程内核对象;
系统在当前进程的中预订一块线程栈空间,并调拨一些物理内存;
线程终止运行
释放线程所拥有的所有用户对象(不太理解)
线程退出代码从STILL_ACTIVE变成真正的退出代码
线程内核对象变为触发状态
如果线程是进程中最后一个活动线程,进程将终止
线程内核对象的使用计数减1
线程的创建和终止方法
无论你使用什么编程语言,什么类库,在Windows平台下最终创建线程都应该有下面的API
1
2
3
4
5
6
7
8
|
HANDLE WINAPI CreateThread( __in_opt LPSECURITY_ATTRIBUTES lpThreadAttributes, //安全描述符,一般传入NULL,但其中的bInheritHandle标志位说明线程内核对象是否允许子进程继承 __in SIZE_T dwStackSize, //线程栈初始化大小,该值可以传入0,系统会从/STACK链接选项和此值两个中选一个较大的 __in LPTHREAD_START_ROUTINE lpStartAddress, //线程执行的初始函数的地址 __in_opt LPVOID lpParameter, //传入初始函数的参数 __in DWORD dwCreationFlags, //指定线程是否能被立即调度(即是否立即执行),如果为CREATE_SUSPENDED,系统会在初始化后暂停线程的运行 __out_opt LPDWORD lpThreadId //线程ID,可以传入NULL ); |
需要说明的是线程初始函数总是拥有如下函数签名:
1
2
3
|
DWORD WINAPI ThreadProc( __in LPVOID lpParameter ); |
该函数返回创建好的线程内核对象的句柄。除非该句柄值将用作他用,否则应该立即调用CloseHandle来关闭句柄,当然关闭句柄不会终止线程的运行,但可以保证线程在退出后即使的释放线程内核对象。
终止线程的方式有线程初始函数返回、线程自己调用ExitThread终止自己、外部线程调用TerminateThread、包含线程的进程终止运行。其中除了第一种方法,其他都是不推荐的方式,因为线程不正常退出不能保证资源的正确释放
使用ExitThread还能保证系统销毁线程的堆栈,但TerminateThread将无法做到,直到进程终止;
线程的初始化内幕
CreateThread导致系统创建一个线程内核对象,该对象的初始引用计数为2。线程正常退出将递减一次,关闭线程句柄将递减一次,引用计数为0时,操作系统会释放改内核对象。暂停计数设置为1,退出代码设置为STILL_ACTIVE,对象被设置为未触发状态。
系统从进程地址空间中分配线程栈,并在高位写入pvParam和pfnStratAddr。
每个线程都有其自己的一组CPU寄存器,称为线程上下文。上下文反映了线程上一次执行时,线程的CPU寄存器状态。当线程被重新调度时,保存在内核对象中的上下文将回写到CPU寄存器,以恢复线程的最后状态,这个过程称为“上下文切换”。其中最为重要的两个寄存器是堆栈寄存器(SP)和指令寄存器(IP),SP指向pfnStartAddr在堆栈中的地址,IP指向RtlUserThreadStart函数(NTDLL.dll导入)。Windows实际上提供了一个描述线程上下文的结构CONTEXT,并且提供了如下两个函数获得和设置上下文:
1
2
3
4
5
6
7
8
9
|
BOOL WINAPI GetThreadContext( __in HANDLE hThread, __inout LPCONTEXT lpContext ); BOOL WINAPI SetThreadContext( __in HANDLE hThread, __in const CONTEXT *lpContext ); |
CONTEXT结构可能是Windows平台上唯一一个跟CPU有关的结构,所以如果要设置该结构可能需要考虑不同CPU的情况,而且如果设置不当,很有可能导致灾难性的后果。
线程完全初始化好之后,系统将检查CreateThread函数中的dwCreationFlags,如果此标记不是CREATE_SUSPENDED,系统将把挂起计数递减至0,以便处理器调度该线程。
Microsoft C/C++运行库注意事项
Visual Studio附带了4个C/C++运行库用于开发,还有两个面向.NET托管环境。现在所有的库都支持多线程开发,已经没有专门针对单线程开发的运行库(C\C++运行库的出现早于多任务操作系统,因此当时有支持单线程的运行库):
库名称 | 描述 |
LibCMt.lib | 库的静态链接发行版本 |
LibCMtD.lib |
库的静态链接调试版本 |
MSVCRt.lib | 导入库,用于动态链接MSVCRxxx.dll库的发行版本 |
MSVCRtD.lib | 导入库,用于动态链接MSVCRxxxD.dll库的调试版本(默认) |
MSVCMRt.lib | 导入库,用户托管/本机代码混合 |
MSVCURt.lib | 导入库,编译成百分之百的纯MSIL代码 |
始终应该用运行库的_beginthreadex来创建线程,以及用_endthreadex来代替ExitThread。
线程的挂起、恢复和睡眠
如上文所讨论的,线程在创建的时候可以设置是否挂起。我们可以通过如下两个函数来挂起和恢复线程
1
2
3
4
5
6
7
|
DWORD WINAPI SuspendThread( __in HANDLE hThread ); DWORD WINAPI ResumeThread( __in HANDLE hThread ); |
调用SuspendThread将递增线程内核对象的挂起计数,可以挂起多次,对应的也可以恢复多次。另外,没有完美的“挂起进程”的函数,因为唯一的方法就是遍历并挂起进程中的所有线程,然而在遍历的过程中如果有新的线程创建了呢,亦或是刚遍历的线程销毁了呢。因此试图“挂起进程”要十分小心,尽量避免。
使用下面的函数指定挂起当前线程一段时间
1
2
3
|
VOID WINAPI Sleep( __in DWORD dwMilliseconds ); |