浅谈 Windows 编程中的 Thread

线程对于 Windows 编程人员来说,并不陌生,但是一直以来,我对它的了解也只是基本的使用层面。对于很多细节,也并不是很了解。这作为一个 Windows 客户端开发人员,可以说是非常尴尬了。所以,抽了一点时间,仔细梳理了一下线程相关的内容。顺便记录下来。

一些常识

  • 基本状态:就绪,执行,阻塞
  • 堆公有、栈私有
  • 创建和结束所需要的系统开销:小
  • 没有自己的地址空间

创建线程

在 Windows 下创建一个线程,很自然的会想到

1
2
3
4
5
6
7
8
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 应用开发人员并不陌生。当使用这个方法的时候,在平时使用的时候,比较多关注的就是lpStartAddresslpParameter。这是线程函数的入口以及参数。创建一个新线程之后,将会从这里开始执行。

但是对于 C++ 来说,其实有另一个方法

1
2
3
4
5
6
7
8
_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 多一些了解,才能写出更高质量的代码。