C C++中ThreadLocal的实现方式

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 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 数据结构,会导致内存泄露。

你可能感兴趣的:(计算机原理性的东西)