线程对于 Windows 编程人员来说,并不陌生,但是一直以来,我对它的了解也只是基本的使用层面。对于很多细节,也并不是很了解。这作为一个 Windows 客户端开发人员,可以说是非常尴尬了。所以,抽了一点时间,仔细梳理了一下线程相关的内容。顺便记录下来。
一些常识
- 基本状态:就绪,执行,阻塞
- 堆公有、栈私有
- 创建和结束所需要的系统开销:小
- 没有自己的地址空间
创建线程
在 Windows 下创建一个线程,很自然的会想到
CreateThread(
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_ SIZE_T dwStackSize,
_In_ LPTHREAD_START_ROUTINE lpStartAddress,
_In_opt_ __drv_aliasesMem LPVOID lpParameter,
_In_ DWORD dwCreationFlags,
_Out_opt_ LPDWORD lpThreadId
);
这个方法可以说对 Windows 应用开发人员并不陌生。当使用这个方法的时候,在平时使用的时候,比较多关注的就是 lpStartAddress
、lpParameter
。这是线程函数的入口以及参数。创建一个新线程之后,将会从这里开始执行。
但是对于 C++ 来说,其实有另一个方法
_ACRTIMP uintptr_t __cdecl _beginthreadex(
_In_opt_ void* _Security,
_In_ unsigned _StackSize,
_In_ _beginthreadex_proc_type _StartAddress,
_In_opt_ void* _ArgList,
_In_ unsigned _InitFlag,
_Out_opt_ unsigned* _ThrdAddr
);
在这里,_StartAddress
、_ArgList
则跟上述那两个参数是类似的作用。
然而在这两个方法的选择中,《Windows 核心编程》早有公断。
根据作者的说法是选择 _beginthreadex
替代 CreateThread
。而原因则要从 _beginthreadex
的实现上说起。
_beginthreadex
在 Windows 下的实现也是调用了 CreateThread
,毕竟在 Windows 系统中,只认这一种创建线程的方式。但是在这之前,它还会做一些额外工作。创建一个线程数据块( tiddata ),然后将入口和参数都保存到数据块中,最后还要把数据块保存在 TLS 中。之后还要初始化一个 SEH 帧,用来处理运行时产生的错误。然后在线程结束之前,释放掉 tiddata 。那这样看,确实要比 CreateThread
多做一些事情。
话说回来,如果不做这些事情,当然就会有问题。比较直接的问题就是内存泄漏。原因是,如果使用 CreateThread
创建线程,当调用一些运行库函数的时候,会检查这个 tiddata 。如果发现没有,则会自己搞出一个,而这个在线程结束的时候,就不会被正确释放,就出现了内存泄漏。
类似 errno
这种运行库函数,需要反应正确的错误信息,如果不记录线程相关信息,则会在多线程的时候出现错误,所以一个 tiddata 是必要的,这也说明了为什么这个 tiddata 无论什么情况都会存在。
所以综上所述,在创建线程是,应该选择 _beginthreadex
。
关于更详细的 _beginthreadex
内容,参考 _beginthread, _beginthreadex 这篇文章是最好了
TLS
上边说的 TLS。可谓是线程中不可缺少的东西。因为线程之间是共享地址空间的,所以当有一些每个线程自己所需要的数据的时候,就不那么方便。而 TLS 就是用来解决这个问题。存储在 TLS 中的数据,对于每个线程之间,是互相隔离的。
结束线程
尽可能的让线程执行完自然结束。不到万不得已的时候,都不要使用 ExitThread
或者是 _endthreadex
。因为会使主调线程不正常返回,导致构造的 C++ 对象都不会析构;如果使用 ExitThread
还会造成 tiddata 不会被释放。
后记
关于多线程编程其实坑不算少,唯有对 Thread 多一些了解,才能写出更高质量的代码。