程序员自我修养-CRT中的多线程

来源:互联网 发布:pandorabox域名过滤 编辑:程序博客网 时间:2024/05/22 00:11

线程的访问权限:

 

多线程运行库:

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函数。

原创粉丝点击