我们之前讲了Windows的线程创建方式,在创建线程时应该使用_beginthreadex函数,而不是CreateThread函数,结束线程应该用_endthreadex,当然最好是不要主动去结束,而是应该用return来结束线程。
C是不支持多线程的,因为在C开发出来的时候,多任务操作系统还没有开发出来,我们现在的处理器都是多线程的,所以现在的程序都要支持多线程,多线程程序中有一个概念是很重要的,那就是threadlocal,我们都知道在C中有一个errno全局变量,可以让我们通过GetLastError()方法来获取错误代码,但是如果是多线程的程序的话,很多线程都同时在改这一个变量,很可能我们获取到的就是其他线程设置的errno了,threadlocal就是要解决这种问题的,有了threadlocal每个线程中的errno都是他自己的,其他线程无法更改你的errno。
上面我们了解了ThreadLocal的作用,就是要保存每个线程自己的变量副本,每个线程都有一个自己的线程运行栈(是用户空间还是内核空间呢?待考察–),堆空间是所有线程共有的进程的用户空间。
如果我们创建线程时用_beginthreadex函数的话,这个函数内部会为我们创建一个 _tiddata结构,如果是我们可以将需要作为线程独立的变量放在这个区域,这就可以使用TlsGetValue来获取threadlocal变量。
我们来看看_beginthreadex的伪代码,其实这个函数的源码是可以看到的,在VC的crt/src/Threadex.c中,这里的伪代码更加的结构更加清晰
uintptr_r __cdecl _beginthreadex
(
void *psa,
unsigned cbStackSize;
unsigned (__stdcall * pfnStartAddr)(void*),
void *pvParam,
unsigned dwCreateFlags,
unsigned *pwdThreadID
)
{
_ptiddata ptd; // pointer to thread's data block
uintptr_r thdl; //Thread's HANDLE
//Allocate data block for new thread
//为新线程分配数据区域
if((ptd=(_ptiddata)_calloc_crt(1,sizeof(struct _tiddata))) == NULL) goto error_return;
// Initilize the data block
initptd(ptd);
//Save the desired thread function and the parameter we want to get in the data block
//将线程的函数和参数放入这块数据区域
ptd->_initaddr = (void *)pfnStartAddr;
ptd->_initarg = pvParam;
ptd->_thandle = (uintptr_t)(-1);
//Create the thread
thdl = (uintptr_t)CreateThread((LPSECURITY_ATTRIBUTES)pas,cbStackSize,
_threadstartex,(PVOID)ptd,dwCreateFlags,pwdThreadID);
if(thdl == 0){
// thread can not be created
goto error_return;
}else{
//返回线程句柄
return (thdl);
}
error_return:
// Error: data block or thread can not be created
_free_crt(ptd);
return ((uintptr_t))L);
}
通过上面的伪代码,我们了解到调用_beginthreadex 函数为我们在堆区创建了一块数据区域,并且把我们的线程函数和参数放入了这块区域,然后调用CreateThread函数时,传入的并不是我们指定的线程函数,而是另一个函数 _threadstartex,而且参数是分配的data block,而不是我们传入的参数,接下来我们来 看看分配的数据区域相关的代码。这个结构体在mtdll.h文件中
struct _tiddata {
unsigned long _tid; /* thread ID */
uintptr_t _thandle; /* thread handle */
int _terrno; /* errno value */
unsigned long _tdoserrno; /* _doserrno value */
unsigned int _fpds; /* Floating Point data segment */
unsigned long _holdrand; /* rand() seed value */
char * _token; /* ptr to strtok() token */
wchar_t * _wtoken; /* ptr to wcstok() token */
unsigned char * _mtoken; /* ptr to _mbstok() token */
/* following pointers get malloc'd at runtime */
char * _errmsg; /* ptr to strerror()/_strerror() buff */
wchar_t * _werrmsg; /* ptr to _wcserror()/__wcserror() buff */
char * _namebuf0; /* ptr to tmpnam() buffer */
wchar_t * _wnamebuf0; /* ptr to _wtmpnam() buffer */
char * _namebuf1; /* ptr to tmpfile() buffer */
wchar_t * _wnamebuf1; /* ptr to _wtmpfile() buffer */
char * _asctimebuf; /* ptr to asctime() buffer */
wchar_t * _wasctimebuf; /* ptr to _wasctime() buffer */
void * _gmtimebuf; /* ptr to gmtime() structure */
char * _cvtbuf; /* ptr to ecvt()/fcvt buffer */
unsigned char _con_ch_buf[MB_LEN_MAX];
/* ptr to putch() buffer */
unsigned short _ch_buf_used; /* if the _con_ch_buf is used */
/* following fields are needed by _beginthread code */
void * _initaddr; /* initial user thread address */
void * _initarg; /* initial user thread argument */
/* following three fields are needed to support signal handling and
* runtime errors */
void * _pxcptacttab; /* ptr to exception-action table */
void * _tpxcptinfoptrs; /* ptr to exception info pointers */
int _tfpecode; /* float point exception code */
/* pointer to the copy of the multibyte character information used by
* the thread */
pthreadmbcinfo ptmbcinfo;
/* pointer to the copy of the locale informaton used by the thead */
pthreadlocinfo ptlocinfo;
int _ownlocale; /* if 1, this thread owns its own locale */
/* following field is needed by NLG routines */
unsigned long _NLG_dwCode;
/*
* Per-Thread data needed by C++ Exception Handling
*/
void * _terminate; /* terminate() routine */
void * _unexpected; /* unexpected() routine */
void * _translator; /* S.E. translator */
void * _purecall; /* called when pure virtual happens */
void * _curexception; /* current exception */
void * _curcontext; /* current exception context */
int _ProcessingThrow; /* for uncaught_exception */
void * _curexcspec; /* for handling exceptions thrown from std::unexpected */
#if defined (_M_X64) || defined (_M_ARM)
void * _pExitContext;
void * _pUnwindContext;
void * _pFrameInfoChain;
#if defined (_WIN64)
unsigned __int64 _ImageBase;
unsigned __int64 _ThrowImageBase;
#else /* defined (_WIN64) */
unsigned __int32 _ImageBase;
unsigned __int32 _ThrowImageBase;
#endif /* defined (_WIN64) */
void * _pForeignException;
#elif defined (_M_IX86)
void * _pFrameInfoChain;
#endif /* defined (_M_IX86) */
_setloc_struct _setloc_data;
void * _reserved1; /* nothing */
void * _reserved2; /* nothing */
void * _reserved3; /* nothing */
#ifdef _M_IX86
void * _reserved4; /* nothing */
void * _reserved5; /* nothing */
#endif /* _M_IX86 */
int _cxxReThrow; /* Set to True if it's a rethrown C++ Exception */
unsigned long __initDomain; /* initial domain used by _beginthread[ex] for managed function */
#if defined(_CRT_APP) && !defined(_KERNELX)
HANDLE _winRTThreadHandle; /* App CRT WinRT thread handle */
#else /* _CRT_APP */
int _initapartment; /* if 1, this thread has initialized apartment */
#endif /* _CRT_APP */
_psetloc_downlevel_struct _setloc_downlevel_data;
};
接下来我们来分析 调用CreateThread时的传入的函数 _threadstartex的伪代码
static unsigned long WINAPI _threadstartex(void *ptd)
{
// Note:ptd is the address of this thread’s tiddata block.
//Associate the tiddata block with this thread so _getptd() will be able to find it in _callthreadstartex.
TlsSetValue(__tlsindex,ptd);
// Save this thread ID in the _tiddata block.
((_ptiddata)ptd)->_tid = GetCurrentThreadId();
_callthreadstartex();
// We never get here ; the thread dies in _callthreadstartex.
return (0L);
}
static void _callthreadstartex(void){
_ptiddata ptd;
//获取data block ,(用TlsSetValue设置的)
ptd = _getptd();
//Wrap desired thread function in SEH frame to handle run-time errors and signal support.
__try{
//调用线程函数
_endthreadex(ptd._initaddr( ptd._initarg ));
}
__except(_XcpFilter(GetExceptionCode(),GetExceptionInformation())){
_exit(GetExceptionCode());
}
}
在这里我们看到了函数是如何使用data block和调用我们的线程函数的,我们也应该知道了,程序会将我们的线程函数以及参数放到datablock中去,并且调用操作系统的TlsSetValue将线程与datablock联系起来了,最后我们再来看一下_endthreadex的伪代码
void __cdecl _endthreadex (unsigned retcode)
{
_ptiddata ptd;// pointer to datablock
//Clean up floating-point support
// Get the address of this thread's tiddata block.
ptd = _getptd_noexti();
//Free the data block
if(ptd != NULL) _freeptd(ptd);
// Terminate thread
ExitThread(retcode);
}
到这里为止我们应该知道了线程独立的数据是如何存储的了,下面我们来看一个实例,我们来看看错误代码时如果支持多线程的,来看VC头文件中关于errno的定义
_CRTIMP extern int * __cdecl _errno(void);
#define errno (*_errno());
int * __cdecl _errno(void){
_ptiddata ptd = _getptd_noexti();
if(!ptd)return &ErrnoNoMem;
else return (&ptd->_terrno);
}
我们可以看到,VC中将errno定义成了一个方法(用宏来替换的),然后在上面讲的 data block中来获取的errno,所以是线程独立的。
经过上面的的分析我们在创建线程和结束线程时应该用 _beginthreadstartex和 _endthreadex,因为他会帮我们维护thread local的数据结构,假如有人就直接用CreateThread会怎么样呢?如果线程要用到threadlocal的数据结构,但是由于是使用CreateThread创建的线程,还没有那个data block ,这里C/C++运行库会帮我们创建一个_tiddata的数据结构,这样基本上是可以正常使用的,但是由于没有初始化SEH(异常处理)帧,当我们使用signal函数是,程序会退出整个进程,还有一个问题是,如果退出的时候没有用_endthreadex的话,由于没有释放_tiddata 数据结构,会导致内存泄露。