C C++中ThreadLocal的实现方式

来源:互联网 发布:mac如何隐藏下面的菜单 编辑:程序博客网 时间:2024/06/05 19:42

C C++中ThreadLocal的实现方式

我们之前讲了Windows的线程创建方式,在创建线程时应该使用_beginthreadex函数,而不是CreateThread函数,结束线程应该用_endthreadex,当然最好是不要主动去结束,而是应该用return来结束线程。

C是不支持多线程的,因为在C开发出来的时候,多任务操作系统还没有开发出来,我们现在的处理器都是多线程的,所以现在的程序都要支持多线程,多线程程序中有一个概念是很重要的,那就是threadlocal,我们都知道在C中有一个errno全局变量,可以让我们通过GetLastError()方法来获取错误代码,但是如果是多线程的程序的话,很多线程都同时在改这一个变量,很可能我们获取到的就是其他线程设置的errno了,threadlocal就是要解决这种问题的,有了threadlocal每个线程中的errno都是他自己的,其他线程无法更改你的errno。

ThreadLocal实现原理

上面我们了解了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 blockif(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 数据结构,会导致内存泄露。

0 0
原创粉丝点击