线程的访问权限:
多线程运行库:
1. c语言必须提供多线程的API
2. 有些函数之前设计并不适合多线程,需要改进
Errno,strtok,malloc,new,printf,异常处理等等多线程都是不安全的
在多线程中CRT的改进:
CRT改进:
1. 使用TLS,比如errno在单线程版本中直接返回全局的errno,但是在多线程中返回的是线程的私有变量
2. 加锁:在多线程版本中,线程不安全的函数内部会自动进行加锁,比如printf,malloc等函数,以及异常处理都已经实现加锁
3. 改进函数的调用方式:
char *strtok(char *strToken,constchar *strDelimit)
char *strtok_schar*strToken,const char *strDelimit,char **context)
改进后的strtok添加了一个参数,因为原来的strtok将分割后的字符串,放在一个静态变量中,现在通过context返回。
线程的局部变量存储
在多线程中线程私有的数据有线程的栈,寄存器,但是如果线程想使用全局变量,但是权限是该线程私有的全局变量,不是所有线程共享的。
TLS(线程局部存储):
对于GCC:__thread intnumber; 这个时候这个number就是线程私有的全局变量
对于MSVC:__declspec(thread) int number;
在windows中TLS的实现:
在一般情况下如果一个变量是全局变量,会被放到.bss或者.data中,但是如果被定义成一个tls变量,会被放到.tls段中。当系统启动一个新的线程时,它会从进程的堆中分配一个空间,然后把.tls段中的内容copy到这块空间中,相当于每个线程都有自己独立的.tls副本。对于__declspec(thread)定义的全局变量在每个线程中地址是不一样的。当然还要对每个TLS变量初始化,最后线程结束的时候进行析构。
在PE的数据目录结构中:
有一项为IMAGE_TLS_DIRECTORY_TLS,这里保存了所有的TLS变量的构造函数和析构函数。
线程时如何访问自己的TLS数据的?
每个线程有一个TEB线程环境块,这里保存了线程的堆栈信息,线程ID等,当然还有一个TLS数组,它在TEB中的偏移是0X2C。对于每个线程来说,在x86结构下,FS寄存器保存该线程的TEB地址,所以要得到一个线程的TLS数组只要访问 FS:[0X2C]。
在TLS数组中第一个元素指向tls副本的位置,所以要访问tls变量,通过FS:[0X2C]访问得到TLS数组,然后通过tls变量在TLS数组中的偏移得到tls变量。
显示TLS:
使用关键字__thread和__declspec(thread)定义的TLS全局变量是隐式的方法,因为程序员不需要关心TLS变量的申请,分配赋值,释放。
显示的TLS:程序员必须自己申请TLS变量,每个访问的时候都要调用相应的函数得打变量的地址,最后自己手工释放。
在windows中,TlsAlloc(),TlsGetValue(),TlsSetValue(),TlsFree()这4个函数用来控制tls变量的申请,取值,赋值,释放
相应的Linux中,pthread_key_create(),pthread_getspecific(), pthread_setspecific(), pthread_key_delete().
显示的Tls实现:
上面提到的TLS数组,一般线程大小是64个元素,第一个上面已经知道了指向.tls的副本,其他的可以用来保存显示的TLS,如果64个不够用了,还可以申请1024个,所以最多TLS可以放下1088个元素。显示的Tls分配在堆上,但是用TLS指向它。
但是总体来说,显示的TLS并不推荐使用。
在windwos中:createthreadCreateThread() 和 beginthread()的区别:
beginthread函数封装了CreateThread。看下面源代码,是msvc中的thread.c:
_CRTIMP uintptr_t __cdecl _beginthread (
void (__cdecl * initialcode) (void *),
unsigned stacksize,
void * argument
)
{
_ptiddata ptd; /* pointer to per-thread data ,这个空间在堆上,但是由显示的TLS保存ptd指针*/
uintptr_t thdl; /* thread handle */
unsigned long err = 0L; /* Return from GetLastError() */
/* validation section */
_VALIDATE_RETURN(initialcode != NULL, EINVAL, -1);
/* Initialize FlsGetValue function pointer */
__set_flsgetvalue();
/*
* Allocate and initialize a per-thread data structure for the to-
* be-created thread. 注意这里为_ptiddata在堆上分配了空间
*/
if ( (ptd = (_ptiddata)_calloc_crt(1, sizeof(struct _tiddata))) == NULL )
{
goto error_return;
}
//_tiddata这里主要包含一个线程的ID,线程的句柄,erron,strtok前一次调用的位置,异常处理等一些线程私有的信息。
/*
* Initialize the per-thread data
*/
_initptd(ptd, _getptd()->ptlocinfo);//将ptd传入,进行初始化
ptd->_initaddr = (void *) initialcode;
ptd->_initarg = argument;
#if defined (_M_CEE) || defined (MRTDLL)
if(!_getdomain(&(ptd->__initDomain)))
{
goto error_return;
}
#endif /* defined (_M_CEE) || defined (MRTDLL) */
/*
* Create the new thread. Bring it up in a suspended state so that
* the _thandle and _tid fields are filled in before execution
* starts.
*/
if ( (ptd->_thandle = thdl = (uintptr_t)
CreateThread( NULL, //这里才是真的创建thread
stacksize,
_threadstart,
(LPVOID)ptd,
CREATE_SUSPENDED,
(LPDWORD)&(ptd->_tid) ))
== (uintptr_t)0 )
{
err = GetLastError();
goto error_return;
}
/*
* Start the new thread executing
*/
if ( ResumeThread( (HANDLE)thdl ) == (DWORD)(-1) ) {
err = GetLastError();
goto error_return;
}
/*
* Good return
*/
return(thdl);
/*
* Error return
*/
error_return:
/*
* Either ptd is NULL, or it points to the no-longer-necessary block
* calloc-ed for the _tiddata struct which should now be freed up.
*/
_free_crt(ptd); //free ptd,防止内存泄露
/*
* Map the error, if necessary.
*/
if ( err != 0L )
_dosmaperr(err);
return( (uintptr_t)(-1) );
}
void __cdecl _endthread (
void
)
{
_ptiddata ptd; /* pointer to thread's _tiddata struct */
ptd = _getptd_noexit();
if (ptd) {
/*
* Close the thread handle (if there was one)
*/
if ( ptd->_thandle != (uintptr_t)(-1) )
(void) CloseHandle( (HANDLE)(ptd->_thandle) );
/*
* Free up the _tiddata structure & its subordinate buffers
* _freeptd() will also clear the value for this thread
* of the FLS variable __flsindex.
*/
_freeptd(ptd); //free ptd,防止内存泄露
}
/*
* Terminate the thread
*/
ExitThread(0);
}
分析:为什么通过CreateThread,调用strtok函数不会出错,按理说使用CreateThread并没有像Beginthread函数一样初始化一个struct_tiddata。实际上在strtok函数本身,一开始会调用一个_getptd得到一个线程的struct _tiddata,所以其实我们不必担心struct _tiddata有没有在createthread中有没有创建。我们知道在_endthread中有free一个struct _tiddata,但是在ExitThread并没有释放这个对象。那为什么在动态链接的时候,使用CreateThread和ExitThread也不会struct _tiddata因为有内存泄露。答案:在CRT DLL的入口函数DllMain中,在这个函数,会被每个DLL调用一次,所以在动态链接的版本中会被DllMain释放。但是在静态链接中就没有这么幸运了,所以会导致内存的泄露。
总结:在使用CRT多线程的时候,尽量用包装过的CRT函数。