由于历史原因,所以C/C++运行库并不是为多线程应用程序而设计的,所以为了保证其中的某些变量和函数的安全,那么必须创建一个数据结构,并使之与使用了C/C++运行库函数的每个线程所关联。当在调用C/C++运行库函数时,那些函数必须读取主调自己的线程的数据块,从而避免印象其他线程。
所以当编写C/C++代码时,请调用_beginthreadex,而不要使用CreateThread的大致原因就是如上所述。详细内容请继续往下读
_beginthreadex的函数参数列表与CreateThread一样,但是参数名和类型有所不同。这是因为C/C++程序不应该对Windows系统的类型有任何的依赖。
_beginthreadex也会返回新建线程的句柄,就像CreateThread一样。可以非常方便的替换自己程序中的CreateThread函数,但是数据类型不一样,所以还得添加一些类型转换即可。
透过_beginthreadex函数的源码我们可知,
1.每个线程都有自己专用的_tiddata内存块,它们是从C/C++运行库的堆上分配的。
2.传给_beginthreadex的线程函数地址(pfnStartAddr)保存在_tiddata内存块中。(_tiddata结构可以在Mtdll.h源码中窥探一二)
3._beginthreadex会在内部调用CreateThread。
4.调用CreateThread时传入的地址是_threadstartex(而非传入的pfnStartAddr)。参数地址是_tiddata结构地址,而非pvParam。
5.如果一切顺利,会返回线程的句柄,操作失败会返回0。
初始化并且传入_tiddata后,又怎么将其和线程关联呢?继续往下看
对于_threadstartex函数及其辅助函数__callthreadstartex:
1.新的线程首先执行RtlUserThreadStart,然后在跳转到_threadstartex。(这便于上一篇所讲的"创建线程的内幕"一文中的知识点所契合)
2._threadstartex唯一的参数就是新线程的_tiddata内存块地址。
3.TlsSetValue是一个操作系统函数,作用是将一个值与主调函数关联起来。(这就是所谓的线程局部存储TLS)_threadstartex函数将_tiddata内存块与新建线程关联起来。------华丽的分割线--------
4.无参数的辅助函数__callthreadstartex中,有一个SEH帧,它处理着许多与运行库有关的事情——比如运行时错误(如抛出未被捕获的C++异常)——和c/c++运行库的signal函数。这点很重要,因为用CreateThread创建的线程,调用C/C++运行库的signal函数,其是不能正常工作的。
5.预期要执行的线程函数会被调用,并向其传递预期的参数。(线程的地址和参数都是存在于TLS中保存的_tiddata数据块中,并会在__callthreadstartex中在TLS中获取得到)6.线程函数的返回值被认为是线程的退出代码。注意:函数不是返回_threadstartex然后返回RltUserThreadStart(因为这样_tiddata内存块不会被释放),而是调用_endthreadex,并向其传入线程函数的返回值。
对于_endthreadex,其内部先获取TLS中的_tiddata块,然后释放掉,并调用操作系统的ExitThread函数来实际销毁线程,并传递正确的退出码。
总结:所以现在应该理解为什么不要调用CreateThread了,因为调用CreateThread后,在线程调用一个需要_tiddata块的c/c++运行库函数时(主要通过TlsGetValue),会得到NULL,这时c/c++运行库会为其初始化一个块,然后这个块与线程关联(主要通过TlsSetValue)。这之后,任何使用_tiddata块的函数都能正常使用了。但是,问题还是有的:1.如果线程使用了signal函数,则整个进程会终止掉(上文有详细描述,关于__callthreadstartex的SEH帧没有准备就绪)。2.线程入口点函数返回后,会在RtlUserThreadStart中直接调用ExitThread,其并没有释放掉_tiddata内存块,这会引起内存泄漏。
附加:绝不调用_beginthread。因为,1._beginthread参数较少,不能创建具有安全属性的线程,不能让线程立即挂起等。_endthread也是如此,其退出代码被硬编码为0。
_endthread还存在一个问题,就是在调用ExitThread前,会调用CloseHandle,向其传入新线程的句柄。举个例子,比如你使用了_beginthread创建了一个新的线程,在其之后使用CloseHandle关闭线程句柄。由于_beginthread会调用_endthread函数终止线程,而线程内核对象的初始化计数为2,在_endthread函数中,调用ExitThread前,会调用CloseHandle,也就是说会连续两次递减引用计数,这时引用计数为0,内核对象被系统销毁,再之后在调用CloseHandle关闭线程句柄时,这个传入的句柄是无效的,即CloseHandle函数的调用会以失败告终。所以请用_beginthreadex代替_beginthread,_endthreadex代替_endthread。 _beginthreadex调用的是_endthreadex,_beginthread调用的是_endthread