如何避免使用CreateThread函数导致的内存泄露
推荐于2016-03-17
展开全部
这得从C运行时库说起了。
VC运行时库,有一个宏errno,用来获得上一步操作的错误码,类似于Win32中的GetLastError()函数。在多线程环境下,不同线程调用errno返回的都是caller线程的错误码,绝对不会混淆,这是因为使用了TLS技术。
TLS,Thread Local Storage,是用来存取线程相关数据的一种技术,在Win32中由操作系统的Tls*系列函数提供支持。例如,可以在程序开始的地方调用TlsAlloc()函数,获得一个TLS index,这个index在进程范围内有效,然后可以创建n个线程,在每个线程中使用TlsSetValue(index,data)将线程相关数据和index关联起来,使用TlsGetValue(index)来获取当前线程和index相关联的的线程相关数据。
查看msdn可以发现,Tls*函数的定义如下:
[cpp] view plaincopy
DWORD WINAPI TlsAlloc(void);
BOOL WINAPI TlsSetValue(
__in DWORD dwTlsIndex,
__in LPVOID lpTlsValue
);
LPVOID WINAPI TlsGetValue(
__in DWORD dwTlsIndex
);
BOOL WINAPI TlsFree(
__in DWORD dwTlsIndex
);
观察TlsSetValue/TlsGetValue的原型可以发现,与index关联的数据只能是void *类型,因此通常的做法是在线程开始的时候,为这个线程分配一块内存,用于存储所有与线程相关的数据,然后把这块内存的起始地址与index关联起来。如果这块内存在线程退出的时候没有释放掉,那就有内存泄露的危险。
回到errno,来看看C运行时库是如何实现errno的。
errno的声明和实现如下:
[c-sharp] view plaincopy
/* error.h - errno的声明 */
_CRTIMP extern int * __cdecl _errno(void);
#define errno (*_errno())
/* dosmap.c - errno的实现 */
int * __cdecl _errno(
void
)
{
_ptiddata ptd = _getptd_noexit();
if (!ptd) {
return &ErrnoNoMem;
} else {
return ( &ptd->_terrno );
}
}
观察_errno的代码,函数首先调用了_getptd_noexit()函数,这个函数的代码如下:
[cpp] view plaincopy
/* tiddata.c - _getptd_noexit()实现 */
_ptiddata __cdecl _getptd_noexit (
void
)
{
_ptiddata ptd;
DWORD TL_LastError;
TL_LastError = GetLastError();
#ifdef _M_IX86
/*
* Initialize FlsGetValue function pointer in TLS by calling __set_flsgetvalue()
*/
if ( (ptd = (__set_flsgetvalue())(__flsindex)) == NULL ) {
#else /* _M_IX86 */
if ( (ptd = FLS_GETVALUE(__flsindex)) == NULL ) {
#endif /* _M_IX86 */
/*
* no per-thread data structure for this thread. try to create
* one.
*/
#ifdef _DEBUG
extern void * __cdecl _calloc_dbg_impl(size_t, size_t, int, const char *, int, int *);
if ((ptd = _calloc_dbg_impl(1, sizeof(struct _tiddata), _CRT_BLOCK, __FILE__, __LINE__, NULL)) != NULL) {
#else /* _DEBUG */
if ((ptd = _calloc_crt(1, sizeof(struct _tiddata))) != NULL) {
#endif /* _DEBUG */
if (FLS_SETVALUE(__flsindex, (LPVOID)ptd) ) {
/*
* Initialize of per-thread data
*/
_initptd(ptd,NULL);
ptd->_tid = GetCurrentThreadId();
ptd->_thandle = (uintptr_t)(-1);
}
else {
/*
* Return NULL to indicate failure
*/
_free_crt(ptd);
ptd = NULL;
}
}
}
SetLastError(TL_LastError);
return(ptd);
}
_getptd_noexit()函数首先通过TLS查找线程相关数据,如果没有找到,就分配一块内存,存放_tiddata结构,并将这块内存与__flsindex相关联。由此可见,errno的确使用了TLS技术,而且通过查找_getptd_noexit() 可以发现,VC运行时库中很多很多函数都使用了TLS,errno只不过是其中的一个典型。
可以猜测一下,当使用CreateThread函数创建线程后,线程函数并不会做C运行时库的初始化,而当线程函数调用errno或localtime或其他需要TLS支持的函数时,这些函数会调用_getptd_noexit()函数初始化一个VC运行时库的TLS数据,当线程函数退出时,这块内存不会自动释放,因此产生了泄漏。
接下来看一下_beginthread/_beginthreadex函数,看看这两个函数如何处理TLS数据:
[cpp] view plaincopy
_MCRTIMP uintptr_t __cdecl _beginthread (
void (__CLRCALL_OR_CDECL * initialcode) (void *),
unsigned stacksize,
void * argument
)
{
_ptiddata ptd;
...
__set_flsgetvalue();
if ( (ptd = (_ptiddata)_calloc_crt(1, sizeof(struct _tiddata))) == NULL )
{
...
}
...
ptd->_initaddr = (void *) initialcode;
ptd->_initarg = argument;
...
if ( (ptd->_thandle = thdl = (uintptr_t)
CreateThread( NULL,
stacksize,
_threadstart,
(LPVOID)ptd,
CREATE_SUSPENDED,
(LPDWORD)&(ptd->_tid) ))
== (uintptr_t)0 )
{
...
}
if ( ResumeThread( (HANDLE)thdl ) == (DWORD)(-1) ) {
...
}
...
}
static unsigned long WINAPI _threadstart (
void * ptd
)
{
_ptiddata _ptd;
...
if ( (_ptd = (_ptiddata)__fls_getvalue(__get_flsindex())) == NULL)
{
if ( !__fls_setvalue(__get_flsindex(), ptd) )
{
ExitThread(GetLastError());
}
}
else
{
..
}
...
_callthreadstart();
return(0L);
}
static void _callthreadstart(void)
{
_ptiddata ptd;
ptd = _getptd();
__try
{
( (void(__CLRCALL_OR_CDECL *)(void *))(((_ptiddata)ptd)->_initaddr) )
( ((_ptiddata)ptd)->_initarg );
_endthread();
}
__except ( ... )
{
...
}
}
void __cdecl _endthread (
void
)
{
_ptiddata ptd;
ptd = _getptd_noexit();
if (ptd) {
if ( ptd->_thandle != (uintptr_t)(-1) )
(void) CloseHandle( (HANDLE)(ptd->_thandle) );
_freeptd(ptd);
}
ExitThread(0);
}
以_beginthread函数为例,如上所示的精简后的代码,整个流程如下:
1. 分配一块内存ptd用于存储_tiddata,并将之初始化,用户所指定的线程函数和参数被存放于ptd中
2. 创建线程,线程的启动函数为_threadstart函数,参数为ptd
3. _threadstart函数在将ptd设置为TLS数据后,调用_callthreadstart()函数
4. _callthreadstart()函数调用用户指定的线程函数,并传入用户指定的参数,然后调用_endthread()函数
5. _endthread()中,调用_freeptd(ptd),释放步骤1中分配的ptd
由此可见:
1. 使用_beginthread(ex)的理由在于这个函数对CRT的TLS数据进行了适当的分配和释放操作,避免内存泄露
2. 内存泄露存在的原因,是由CreateThead创建的线程不会去检查CRT的TLS数据是否需要释放
适当的处理
在了解了CreateThread导致内存泄露的原因后,我简单考虑了一下避免这种内存泄露的方法。
首先老老实实的使用_beginthead(ex)函数,是最稳妥的办法。
其次能否避免使用CRT中依赖TLS的函数呢?
也许可以,但是我们所书写的代码,不完全是我们自己在用,而且我们CreateThead所创建的线程,也不一定跑的都是自己的代码,例如我们提供一个库给别人使用,难道还要特别说明不允许使用errno/localtime等函数么?因此这个方法是不建议的。
如果我想用CreateProcess,或者我所使用的底层库使用的是CreateProcess函数,我又不可避免的会使用依赖于TLS的VC运行时库函数,有什么办法能保证ptd会被释放呢?
我们可以自己释放ptd。前面的分析可以看出,_endthread()函数调用了_freeptd(ptd)来释放ptd,因此我们可以在线程函数的末尾显示的调用_endthread()或_endthreadex(retcode)函数来释放ptd。在查看了_freeptd函数的代码后,我发现如果传入参数是NULL,_freeptd函数释放的就是caller线程的ptd,因此也可以直接调用_freeptd来执行清理。
另外,我们也可以自动释放ptd。在VC的工程属性中,可以选择运行时库的类型,如图:
如果我们选择/MTd或/MDd,运行时库以动态方式链接,即我们的程序会使用传说中的msvcrt*.dll。在这个dll的入口函数中,会在DLL_THREAD_ATTACH时为attach的线程初始化TLS数据,在DLL_THREAD_DETACH时为detach的线程调用_freeptd函数执行清理。因此如果我们使用VC的动态库,使用CreateThread和使用_beginthread是同样安全的。
总结一下,避免CreateThread引发泄露,大致有几种方法:
1. 使用_beginthread/_beginthreadex函数创建线程
2. 在线程函数return前,显示调用_endthread/_endthreadex函数
3. 在线程函数return前,显示调用_freeptd(NULL),此方法在C语言中有效
4. 使用/MTd或/MDd参数
补充
如果没有特殊的理由,一定不要使用TerminateThread函数来终止线程。在连接静态库的情况下,TerminateThread会使线程直接被终止,因此没有释放ptd的机会;在连接静态库的情况下,DLL_THREAD_DETACH只有在线程正常退出时才会产生,因此被终止的线程的ptd同样不会被释放。
引用的代码请参考VC crt的源代码。
VC运行时库,有一个宏errno,用来获得上一步操作的错误码,类似于Win32中的GetLastError()函数。在多线程环境下,不同线程调用errno返回的都是caller线程的错误码,绝对不会混淆,这是因为使用了TLS技术。
TLS,Thread Local Storage,是用来存取线程相关数据的一种技术,在Win32中由操作系统的Tls*系列函数提供支持。例如,可以在程序开始的地方调用TlsAlloc()函数,获得一个TLS index,这个index在进程范围内有效,然后可以创建n个线程,在每个线程中使用TlsSetValue(index,data)将线程相关数据和index关联起来,使用TlsGetValue(index)来获取当前线程和index相关联的的线程相关数据。
查看msdn可以发现,Tls*函数的定义如下:
[cpp] view plaincopy
DWORD WINAPI TlsAlloc(void);
BOOL WINAPI TlsSetValue(
__in DWORD dwTlsIndex,
__in LPVOID lpTlsValue
);
LPVOID WINAPI TlsGetValue(
__in DWORD dwTlsIndex
);
BOOL WINAPI TlsFree(
__in DWORD dwTlsIndex
);
观察TlsSetValue/TlsGetValue的原型可以发现,与index关联的数据只能是void *类型,因此通常的做法是在线程开始的时候,为这个线程分配一块内存,用于存储所有与线程相关的数据,然后把这块内存的起始地址与index关联起来。如果这块内存在线程退出的时候没有释放掉,那就有内存泄露的危险。
回到errno,来看看C运行时库是如何实现errno的。
errno的声明和实现如下:
[c-sharp] view plaincopy
/* error.h - errno的声明 */
_CRTIMP extern int * __cdecl _errno(void);
#define errno (*_errno())
/* dosmap.c - errno的实现 */
int * __cdecl _errno(
void
)
{
_ptiddata ptd = _getptd_noexit();
if (!ptd) {
return &ErrnoNoMem;
} else {
return ( &ptd->_terrno );
}
}
观察_errno的代码,函数首先调用了_getptd_noexit()函数,这个函数的代码如下:
[cpp] view plaincopy
/* tiddata.c - _getptd_noexit()实现 */
_ptiddata __cdecl _getptd_noexit (
void
)
{
_ptiddata ptd;
DWORD TL_LastError;
TL_LastError = GetLastError();
#ifdef _M_IX86
/*
* Initialize FlsGetValue function pointer in TLS by calling __set_flsgetvalue()
*/
if ( (ptd = (__set_flsgetvalue())(__flsindex)) == NULL ) {
#else /* _M_IX86 */
if ( (ptd = FLS_GETVALUE(__flsindex)) == NULL ) {
#endif /* _M_IX86 */
/*
* no per-thread data structure for this thread. try to create
* one.
*/
#ifdef _DEBUG
extern void * __cdecl _calloc_dbg_impl(size_t, size_t, int, const char *, int, int *);
if ((ptd = _calloc_dbg_impl(1, sizeof(struct _tiddata), _CRT_BLOCK, __FILE__, __LINE__, NULL)) != NULL) {
#else /* _DEBUG */
if ((ptd = _calloc_crt(1, sizeof(struct _tiddata))) != NULL) {
#endif /* _DEBUG */
if (FLS_SETVALUE(__flsindex, (LPVOID)ptd) ) {
/*
* Initialize of per-thread data
*/
_initptd(ptd,NULL);
ptd->_tid = GetCurrentThreadId();
ptd->_thandle = (uintptr_t)(-1);
}
else {
/*
* Return NULL to indicate failure
*/
_free_crt(ptd);
ptd = NULL;
}
}
}
SetLastError(TL_LastError);
return(ptd);
}
_getptd_noexit()函数首先通过TLS查找线程相关数据,如果没有找到,就分配一块内存,存放_tiddata结构,并将这块内存与__flsindex相关联。由此可见,errno的确使用了TLS技术,而且通过查找_getptd_noexit() 可以发现,VC运行时库中很多很多函数都使用了TLS,errno只不过是其中的一个典型。
可以猜测一下,当使用CreateThread函数创建线程后,线程函数并不会做C运行时库的初始化,而当线程函数调用errno或localtime或其他需要TLS支持的函数时,这些函数会调用_getptd_noexit()函数初始化一个VC运行时库的TLS数据,当线程函数退出时,这块内存不会自动释放,因此产生了泄漏。
接下来看一下_beginthread/_beginthreadex函数,看看这两个函数如何处理TLS数据:
[cpp] view plaincopy
_MCRTIMP uintptr_t __cdecl _beginthread (
void (__CLRCALL_OR_CDECL * initialcode) (void *),
unsigned stacksize,
void * argument
)
{
_ptiddata ptd;
...
__set_flsgetvalue();
if ( (ptd = (_ptiddata)_calloc_crt(1, sizeof(struct _tiddata))) == NULL )
{
...
}
...
ptd->_initaddr = (void *) initialcode;
ptd->_initarg = argument;
...
if ( (ptd->_thandle = thdl = (uintptr_t)
CreateThread( NULL,
stacksize,
_threadstart,
(LPVOID)ptd,
CREATE_SUSPENDED,
(LPDWORD)&(ptd->_tid) ))
== (uintptr_t)0 )
{
...
}
if ( ResumeThread( (HANDLE)thdl ) == (DWORD)(-1) ) {
...
}
...
}
static unsigned long WINAPI _threadstart (
void * ptd
)
{
_ptiddata _ptd;
...
if ( (_ptd = (_ptiddata)__fls_getvalue(__get_flsindex())) == NULL)
{
if ( !__fls_setvalue(__get_flsindex(), ptd) )
{
ExitThread(GetLastError());
}
}
else
{
..
}
...
_callthreadstart();
return(0L);
}
static void _callthreadstart(void)
{
_ptiddata ptd;
ptd = _getptd();
__try
{
( (void(__CLRCALL_OR_CDECL *)(void *))(((_ptiddata)ptd)->_initaddr) )
( ((_ptiddata)ptd)->_initarg );
_endthread();
}
__except ( ... )
{
...
}
}
void __cdecl _endthread (
void
)
{
_ptiddata ptd;
ptd = _getptd_noexit();
if (ptd) {
if ( ptd->_thandle != (uintptr_t)(-1) )
(void) CloseHandle( (HANDLE)(ptd->_thandle) );
_freeptd(ptd);
}
ExitThread(0);
}
以_beginthread函数为例,如上所示的精简后的代码,整个流程如下:
1. 分配一块内存ptd用于存储_tiddata,并将之初始化,用户所指定的线程函数和参数被存放于ptd中
2. 创建线程,线程的启动函数为_threadstart函数,参数为ptd
3. _threadstart函数在将ptd设置为TLS数据后,调用_callthreadstart()函数
4. _callthreadstart()函数调用用户指定的线程函数,并传入用户指定的参数,然后调用_endthread()函数
5. _endthread()中,调用_freeptd(ptd),释放步骤1中分配的ptd
由此可见:
1. 使用_beginthread(ex)的理由在于这个函数对CRT的TLS数据进行了适当的分配和释放操作,避免内存泄露
2. 内存泄露存在的原因,是由CreateThead创建的线程不会去检查CRT的TLS数据是否需要释放
适当的处理
在了解了CreateThread导致内存泄露的原因后,我简单考虑了一下避免这种内存泄露的方法。
首先老老实实的使用_beginthead(ex)函数,是最稳妥的办法。
其次能否避免使用CRT中依赖TLS的函数呢?
也许可以,但是我们所书写的代码,不完全是我们自己在用,而且我们CreateThead所创建的线程,也不一定跑的都是自己的代码,例如我们提供一个库给别人使用,难道还要特别说明不允许使用errno/localtime等函数么?因此这个方法是不建议的。
如果我想用CreateProcess,或者我所使用的底层库使用的是CreateProcess函数,我又不可避免的会使用依赖于TLS的VC运行时库函数,有什么办法能保证ptd会被释放呢?
我们可以自己释放ptd。前面的分析可以看出,_endthread()函数调用了_freeptd(ptd)来释放ptd,因此我们可以在线程函数的末尾显示的调用_endthread()或_endthreadex(retcode)函数来释放ptd。在查看了_freeptd函数的代码后,我发现如果传入参数是NULL,_freeptd函数释放的就是caller线程的ptd,因此也可以直接调用_freeptd来执行清理。
另外,我们也可以自动释放ptd。在VC的工程属性中,可以选择运行时库的类型,如图:
如果我们选择/MTd或/MDd,运行时库以动态方式链接,即我们的程序会使用传说中的msvcrt*.dll。在这个dll的入口函数中,会在DLL_THREAD_ATTACH时为attach的线程初始化TLS数据,在DLL_THREAD_DETACH时为detach的线程调用_freeptd函数执行清理。因此如果我们使用VC的动态库,使用CreateThread和使用_beginthread是同样安全的。
总结一下,避免CreateThread引发泄露,大致有几种方法:
1. 使用_beginthread/_beginthreadex函数创建线程
2. 在线程函数return前,显示调用_endthread/_endthreadex函数
3. 在线程函数return前,显示调用_freeptd(NULL),此方法在C语言中有效
4. 使用/MTd或/MDd参数
补充
如果没有特殊的理由,一定不要使用TerminateThread函数来终止线程。在连接静态库的情况下,TerminateThread会使线程直接被终止,因此没有释放ptd的机会;在连接静态库的情况下,DLL_THREAD_DETACH只有在线程正常退出时才会产生,因此被终止的线程的ptd同样不会被释放。
引用的代码请参考VC crt的源代码。
推荐律师服务:
若未解决您的问题,请您详细描述您的问题,通过百度律临进行免费专业咨询