《Windows核心编程》读书笔记六 线程基础

第六章 线程基础


本章内容


6.1 何时创建线程

6.2 何时不应该创建线程

6.3 编写一个线程函数

6.4 CreateThread函数

6.5 终止运行线程

6.6 线程内幕

6.7 C/C++运行库注意事项

6.8 了解自己的身份


线程有两个组成部分:

1)一个是线程的内核对象,操作系统用他管理线程。系统还用内核对象来存放线程统计信息的地方。

2)一个线程栈,用于维护线程执行时所需的所有函数的参数和局部变量

进程只是一个线程容器,进程内的线程共享地址空间,进程的内核对象句柄。

进程需要维护地址空间,加载dll ,exe文件等。操作系统内部需要统计大量的信息。

而线程只需要一个内核对象和一个栈,开销低很多。(在VC++库上还会维护一个_tiddata的Thread Local Storage的数据结构)


6.1 何时创建线程

充分利用cpu资源并行执行任务。将UI和后台处理分割,UI只响应用户输入,处理交给后台的线程。这样能提升用户体验。


6.2 何时不应该创建线程

线程能解决一些问题但是会产生一些新的问题。也就是数据共享问题。(多线程同时访问数据该如何同步防止产生资源竞争)

在用户界面上应该用一个线程来创建各个层级的窗口(很少用多线程创建各自的窗口,Windows的Explorer资源管理器的每个窗口都是一个线程为了防止某个文件操作停止响应而导致完全无法操作管理文件)

通常应用程序由一个用户界面线程处理消息和创建各种窗口(高优先级),其他工作线程处理各种后台运算并且不会创建窗口。

6.3 编写第一个线程函数

每个线程都有一个入口函数

例如

DWORD WINAPI ThreadFunc(PVOID pvParam) {
	DWORD dwResult = 0;

	//...
	return dwResult;
}

线程内核对象的寿命可能超过线程本身的寿命。


主线程函数入口必须为 :  main, wmain, WinMain  wWinMain, 而线程函数可以任意命名


主线程函数有多个参数,而普通线程函数只有一个参数。而且其意义可以由我们自行定义。


线程函数必须有一个返回值,他会成为该线程的退出代码。(主线程的退出代码成为进程的退出代码)


线程函数应该尽量使用函数参数和局部变量。使用静态变量和全局变量,多个线程可以同时访问这些变量,可能破坏变量中保存的内容。(涉及了资源竞争Race Condition)


6.4 CreateThread函数

WINBASEAPI
_Ret_maybenull_
HANDLE
WINAPI
CreateThread(
    _In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
    _In_ SIZE_T dwStackSize,
    _In_ LPTHREAD_START_ROUTINE lpStartAddress,
    _In_opt_ __drv_aliasesMem LPVOID lpParameter,
    _In_ DWORD dwCreationFlags,
    _Out_opt_ LPDWORD lpThreadId
    );
该函数会创建一个线程内核对象,一个较小的数据结构,操作系统用该结构来管理线程。

系统从进程的地址空间中分配内存给线程栈使用。 新线程可以访问进程内核对象的所有句柄,进程中所有内存以及同一个进程中其他所有线程的栈。


作者推荐 在写C/C++代码的时候不要使用CreateThread来创建线程,而应该用C++库的_beginthreadex.

CreateThread提供操作系统级别的创建线程的操作,且仅限于工作者线程。不调用MFC和RTL的函数时,可以用CreateThread,其它情况不要轻易。在使用的过程中要考虑到进程的同步与互斥的关系(防止死锁)。
线程函数定义为:DWORD WINAPI _yourThreadFun(LPVOID pParameter)。

但它没有考虑:
(1)C Runtime中需要对多线程进行纪录和初始化,以保证C函数库工作正常(典型的例子是strtok函数)。
(2)MFC也需要知道新线程的创建,也需要做一些初始化工作(当然,如果没用MFC就没事了)。 
同时_beginthreadex呢:
MS对C Runtime库的扩展SDK函数,首先针对C Runtime库做了一些初始化的工作,以保证C Runtime库工作正常。然后,调用CreateThread真正创建线程。 仅使用Runtime Library时,可以用_BegingThreadex。

在_beginthreadex的源码中(后面有贴)。可以看出最重要的一个步骤是调用 _calloc_crt来创建一个线程特有的数据结构和线程私有的数据结构(thread local storage)_tiddata, 比如多线程的strok等。

总结:如果你要在自己创建的线程函数中使用大量C/C++标准库的功能就必须使用_beginthreadex防止出错

如果只是单纯的使用windows api 不涉及任何标志C/C++库函数可以调用CreateThread

6.4.1 psa 参数

一个指向SECURITY_ATTRIBUTES结构的指针。默认的安全属性可以传入NULL。 若要自己从能继承此线程内核对象,则必须设定继承属性初始化成员bInheritHandle = TRUE


6.4.2 cbStackSize参数

指定线程可以为其线程栈使用多少地址空间。

CreateProcess创建主线程也会使用此值,保存在exe文件内部。 可以用/STACK:[reserve] [,commit]来控制该值。

默认是1MB(在Itanium芯片组上默认4MB) commit 指定最初应该为栈预留的地址空间区域调拨多少物理内存,默认是一个页。


预定的地址空间的容量设定了栈空间的上限,这样才能捕获代码中的无穷递归bug(默认x86 CPU的 ESS, ESP寄存器本身并不设定任何栈空间的上限,这是操作系统为了管理栈所做的限制)也防止线程耗尽进程的内存地址空间。


6.4.3 pfnStartAddr 和 pvParam参数

指定了线程函数的地址, 线程函数的参数与CreateThread的pvParam参数一致。通常用于传递一个初始值给线程函数。这样多个线程可以共用同一个线程函数。通过向其传递不同的初始值来指定不同的任务。

参考以下bug代码,在SecondThread中使用了第一个线程栈上的局部变量。该变量在引用时可能已经被销毁了从而导致内存错误。

DWORD WINAPI SecondThread(PVOID pvParam) {

	// Do some length processing here...
	// attempt to access the  variable on FirstThread's stack.
	// Note:: this may cause an access violation - it depends on timing!.

	*((int *)pvParam) = 5;

	return 0;
}

DWORD WINAPI FirstThread(PVOID pvParam) {
	// Initialize a stack-based variable
	int x = 0;

	DWORD dwThreadID;

	// Create a new thread.
	HANDLE hThread = CreateThread(NULL, 0, SecondThread, (PVOID)&x,
		0, &dwThreadID);

	// we don't reference the new thread anymore.
	// so close our handle to it.
	CloseHandle(hThread);

	// Our thread is done.
	// Bug: our stack will be destroyed, but
	// Secondthread might try to access it.
	return 0;
}

该问题的解决方案是将x申明为一个静态变量,使编译器在应用程序的Section(而不是线程栈)中为x创建一个存储区域。 但是这又会使得当前线程不可重入。(也就是不能再创建同一个线程来执行当前函数)除非使用正确的线程同步技术。


6.4.4 dwCreateFlags

线程的创建标志, CREATE_SUSPENDED 创建完毕以后线程将被挂起。常用语配合作业来完成一些功能限制。


6.4.5 pdwThreadID

一个指向DWORD型的地址,返回创建线程的ThreadID


6.5 终止运行线程

线程可以通过以下4种方法来终止运行。

1)线程函数返回

2)线程通过调用ExitThread函数来“杀死”自己(要避免这种方法)

3)同一个进程或另一个进程的线程调用TerminateThread(要避免这种方法)

4)包含线程的进程终止运行(要避免该方法)


6.5.1 线程函数返回

让线程函数返回,可以确保以下正确的应用程序清理工作都得以执行。

1)线程函数中创建的所有C++对象都通过其析构函数被正确销毁。

2)操作系统正确释放线程栈使用的内存

3)操作系统把线程的退出代码设置为线程函数的返回值

4)系统递减少线程内核对象的使用计数器。


6.5.2 ExitThread函数

WINBASEAPI
DECLSPEC_NORETURN
VOID
WINAPI
ExitThread(
    _In_ DWORD dwExitCode
    );

该函数将终止线程的运行,并且导致操作系统清理该线程所使用的所有操作系统资源。但是标准C/C++库内部维护的资源不会被销毁(通常是_tiddata数据结构和SEH帧)

因此最好是直接让线程函数返回,而不要调用ExitThread。

如果一定要杀死C++线程要用_endthreadex (因为他能正常销毁标准库的c++对象:通常是_tiddata数据结构和SEH帧)


6.5.3 TerminateThread函数

WINBASEAPI
BOOL
WINAPI
TerminateThread(
    _In_ HANDLE hThread,
    _In_ DWORD dwExitCode
    );

ExitThread用于杀死主调线程,TerminateThread能杀死任何线程。hThread表示了那个要被杀死线程的句柄。此函数是异步的,通知杀死目标线程后立即返回。需要调用WaitForSingleObject来确定目标线程是否已经被杀死。


但一个良好设计的应用程序绝不应该使用这个函数,因为被函数杀死的线程收不到他被杀死的通知。线程无法正确清理,而且不能阻止自己被终止运行。

注意ExitThread来终止线程是可以销毁堆栈的。

但TerminateThread,除非该线程的进程终止运行,否则其线程堆栈不会被释放。(这是微软故意设计的,否则如果有其他线程还在引用那个被杀死的线程堆栈上的数据,就会引起访问违规。)

另外DLL库在线程ExitThread时候会收到通知,但TerminateThread则不会收到通知。

看一下如下的测试代码。


class a{
public:
	a(){
		int temp;
		temp = 0;
	}
	~a(){
		int temp;
		temp = 0;
	}
};

DWORD WINAPI SecondThread(PVOID pvParam) {

	a A;
	while (true) {

	}
	return 0;
}
int _tmain(int argc, TCHAR* argv[], TCHAR * env[])
{
	DWORD dwThreadID;
	HANDLE hThread = CreateThread(NULL, 0, SecondThread, NULL,
		0, &dwThreadID);

	Sleep(1000);
	TerminateThread(hThread, 0);
	CloseHandle(hThread);

	while (true) {

	}
	return 0;
}

在主线程中创建了子线程SecondThread, 在SecondThread的栈上创建了对象A。实际调试发现。

A的析构函数永远不会被调用。因为TerminateThread不会销毁目标线程的栈。



6.5.4 进程终止运行时

ExitProcessTerminateProces也可以用于终止线程的运行。该函数会使终止的进程中的所有线程都终止,由于整个进程都会被关闭,所以线程的所有资源肯定会被清理。


6.5.5 线程终止运行时

线程终止运行时会发生以下事情:

1)线程拥有的所有用户对象句柄被释放。在Windows中,大部分用户对象归进程所有。但窗口和钩子归线程所有。

2)线程的退出代码从STILL_ACTIVE编程传递给ExitThread和TerminateThread的值

3)线程内核对象变为触发状态

4)如果线程是进程的最后一个活动线程,那么系统认为进程也终止了

5)线程内核对象的使用计数器递减1


可以调用GetExitCodeThread 来检测hThread所表示的线程是否已经终止并检查其退出代码。

WINBASEAPI
_Success_(return != 0)
BOOL
WINAPI
GetExitCodeThread(
    _In_ HANDLE hThread,
    _Out_ LPDWORD lpExitCode
    );

如果未终止,是STILL_ACTIVE


6.6 线程内幕

系统是如何创建和初始化一个线程


1)CreateThread的调用将创建一个线程内核对象,该对象最初使用计数器=2(除非线程终止,则CreateThread返回的句柄被关闭,否则线程内核对象不会被销毁)

还有其他成员也被初始化如图

2)一旦创建了线程内核对象,系统就分配内存,供线程的堆栈使用。(内存是进程的地址空间内分配的)在其堆栈上写入两个值(从高位到地位:pvParam参数值 和pfnStartAddr线程函数入口地址)

每个线程都有一组与之对应的寄存器值。存放于CONTEXT结构体定义在Winnt.h中

typedef struct _CONTEXT {

    //
    // The flags values within this flag control the contents of
    // a CONTEXT record.
    //
    // If the context record is used as an input parameter, then
    // for each portion of the context record controlled by a flag
    // whose value is set, it is assumed that that portion of the
    // context record contains valid context. If the context record
    // is being used to modify a threads context, then only that
    // portion of the threads context will be modified.
    //
    // If the context record is used as an IN OUT parameter to capture
    // the context of a thread, then only those portions of the thread's
    // context corresponding to set flags will be returned.
    //
    // The context record is never used as an OUT only parameter.
    //

    DWORD ContextFlags;

    //
    // This section is specified/returned if CONTEXT_DEBUG_REGISTERS is
    // set in ContextFlags.  Note that CONTEXT_DEBUG_REGISTERS is NOT
    // included in CONTEXT_FULL.
    //

    DWORD   Dr0;
    DWORD   Dr1;
    DWORD   Dr2;
    DWORD   Dr3;
    DWORD   Dr6;
    DWORD   Dr7;

    //
    // This section is specified/returned if the
    // ContextFlags word contians the flag CONTEXT_FLOATING_POINT.
    //

    FLOATING_SAVE_AREA FloatSave;

    //
    // This section is specified/returned if the
    // ContextFlags word contians the flag CONTEXT_SEGMENTS.
    //

    DWORD   SegGs;
    DWORD   SegFs;
    DWORD   SegEs;
    DWORD   SegDs;

    //
    // This section is specified/returned if the
    // ContextFlags word contians the flag CONTEXT_INTEGER.
    //

    DWORD   Edi;
    DWORD   Esi;
    DWORD   Ebx;
    DWORD   Edx;
    DWORD   Ecx;
    DWORD   Eax;

    //
    // This section is specified/returned if the
    // ContextFlags word contians the flag CONTEXT_CONTROL.
    //

    DWORD   Ebp;
    DWORD   Eip;
    DWORD   SegCs;              // MUST BE SANITIZED
    DWORD   EFlags;             // MUST BE SANITIZED
    DWORD   Esp;
    DWORD   SegSs;

    //
    // This section is specified/returned if the ContextFlags word
    // contains the flag CONTEXT_EXTENDED_REGISTERS.
    // The format and contexts are processor specific
    //

    BYTE    ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];

} CONTEXT;

typedef CONTEXT *PCONTEXT;

CONTEXT数据结构存放于线程内核对象中

CONTEXT中的Esp和SegSs指向了ptnStartAddr在线程堆栈中的地址。(ESP:SS)

CONTEXT中的Eip和SegCs指向RtlUserThreadStart函数(ntdll.dll)导出

void RtlUserThreadStart(PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam)
{
	_try
	{
		ExitThread((pfnStartAddr)(pvParam));
	}
		_except(UnhandledExceptionFilter(GetExceptionInfomation()))
	{
		ExitProcess(GetExceptionCode());
	}
	// NOTE: we never get here!
}

线程完全创建好以后,系统将检查CREATE_SUSPENDED标志是否已被传递给CreateThread函数。如果没有,系统将线程挂机计数递减到0;随后线程就可以调度给cpu去执行。

cpu在轮询执行线程以前先加载CONTEX中的每个寄存器的值,因为CS:IP指向了RtlUserThreadStart 所以该函数是线程开始执行的地方。(两个参数是操作系统显示写入线程堆栈传递的)


新线程执行RtlUserThreadStart函数将发生以下事情。

1)围绕线程函数,会设置一个结构化异常处理(Structured Exception Handling,SEH)帧。这样线程执行过程的任何异常都能得到系统的默认处理

2)系统调用线程函数,把传递给CreateThread函数的pvParam参数传递给它。

3)线程函数返回以后,RtlUserThreadStart调用ExitThread,线程内核对象使用技术递减,然后线程停止执行。

4)如果线程产生一个未被处理的异常,RtlUserThreadStart函数所设置的SEH帧会处理这个异常。通常是显示一个框,在用户关闭以后RtlUserThreadStart会调用ExitProcess来终止进程。


线程函数在完成他的工作以后返回并将线程函数的返回值压入堆栈,是线程函数直到在何处返回。

但是RtlUserThreadStart函数是不允许返回的。如果他在没有强行杀死线程的情况下返回,几乎肯定会引起访问违规,因为线程堆栈上没有函数返回地址。


同样进程的主线程初始化时,其CONTEXT中的CS:IP也被设定为RtlUserThreadStart。 他会调用c/c++中的启动代码。后者再调用mian WinMain函数。

在main函数返回以后C/C++的启动代码再调用ExitProcess。

主线程永远都不会返回RtlUserThreadStart函数


6.7 C/C++运行库注意事项


通过Project->C++->Code Gereration->Runtime Library来选择合适的运行库。

例如在早期标准C库中多个线程同时会访问一个全局变量,errno。对其访问的值未必是当前线程执行了系统函数的正确反映值。(该值不具备Thread Local Storage特性)

还有_doserrno, strtok, _wcstok, strerror, _strerror, tmpnam, tmpfile, asctime, _wasctime, gmtime,_ecvt 和 _fcvt等。


因此有了_beginthreadex函数,该函数内部调用的CRT library会维护(TLS)变量和函数的初始化工作。

_CRTIMP uintptr_t __cdecl _beginthreadex (
        void *security,
        unsigned stacksize,
        unsigned (__stdcall * initialcode) (void *),
        void * argument,
        unsigned createflag,
        unsigned *thrdaddr
        );

因为微软的C/C++团队认为C++运行库函数不应该对操作系统数据类型有任何依赖。宏chBEGINTHREADEX可以直接替换CreateThread执行_beginthreadex

参考_beginthreadex的代码实现

_CRTIMP uintptr_t __cdecl _beginthreadex (
        void *security,
        unsigned stacksize,
        unsigned (__stdcall * initialcode) (void *),
        void * argument,
        unsigned createflag,
        unsigned *thrdaddr
        )
{
        _ptiddata ptd;               /* pointer to per-thread data */
        uintptr_t thdl;              /* thread handle */
        unsigned long err = 0L;      /* Return from GetLastError() */
        unsigned dummyid;            /* dummy returned thread ID */

        /* validation section */
        _VALIDATE_RETURN(initialcode != NULL, EINVAL, 0);

        /*
         * Allocate and initialize a per-thread data structure for the to-
         * be-created thread.
         */
        if ( (ptd = (_ptiddata)_calloc_crt(1, sizeof(struct _tiddata))) == NULL )
                goto error_return;

        /*
         * Initialize the per-thread data
         */

        _initptd(ptd, _getptd()->ptlocinfo);

        ptd->_initaddr = (void *) initialcode;
        ptd->_initarg = argument;
        ptd->_thandle = (uintptr_t)(-1);

#if defined (_M_CEE) || defined (MRTDLL)
        if(!_getdomain(&(ptd->__initDomain)))
        {
            goto error_return;
        }
#endif  /* defined (_M_CEE) || defined (MRTDLL) */

        /*
         * Make sure non-NULL thrdaddr is passed to CreateThread
         */
        if ( thrdaddr == NULL )
                thrdaddr = &dummyid;

        /*
         * Create the new thread using the parameters supplied by the caller.
         */
        if ( (thdl = (uintptr_t)
              _createThread( (LPSECURITY_ATTRIBUTES)security,
                            stacksize,
                            (LPVOID)ptd,
                            createflag,
                            (LPDWORD)thrdaddr))
             == (uintptr_t)0 )
        {
                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);

        /*
         * Map the error, if necessary.
         *
         * Note: this routine returns 0 for failure, just like the Win32
         * API CreateThread, but _beginthread() returns -1 for failure.
         */
        if ( err != 0L )
                _dosmaperr(err);

        return( (uintptr_t)0 );
}

要点:

1)每个线程都有自己的_tiddata内存块,是从c/c++运行库的堆上分配的。

2)传递_beginthreadex 的线程函数地址保存在_tiddaa内存块中(保存了一些线程相关的统计信息)。

3)在_beginthreadex内部传递给CreateThread的函数地址是_threadstartex而非_pfnStartAddr,参数是_tiddata结构的地址

(但是实际上_ptfnStartAddr 和pvParam都已经被保存在_tiddata结构体内部了)

接着_threadstartex

static unsigned long WINAPI _threadstartex (
        void * ptd
        )
{
        _ptiddata _ptd;                  /* pointer to per-thread data */

        /*
         * Check if ptd is initialised during THREAD_ATTACH call to dll mains
         */
        if ( ( _ptd = (_ptiddata)__crtFlsGetValue(__get_flsindex())) == NULL)
        {
            /*
             * Stash the pointer to the per-thread data stucture in TLS
             */
            if ( !__crtFlsSetValue(__get_flsindex(), ptd) )
                ExitThread(GetLastError());
            /*
             * Set the thread ID field -- parent thread cannot set it after
             * CreateThread() returns since the child thread might have run
             * to completion and already freed its per-thread data block!
             */
            ((_ptiddata) ptd)->_tid = GetCurrentThreadId();
            _ptd = ptd;
        }
        //...
}

该函数调用系统函数记录每个线程的thread local storage



接着该函数会调用_callthreadex函数来启动线程

static void _callthreadstartex(void)
{
    _ptiddata ptd;           /* pointer to thread's _tiddata struct */

    /* must always exist at this point */
    ptd = _getptd();

    /*
        * Guard call to user code with a _try - _except statement to
        * implement runtime errors and signal support
        */
    __try {
            _endthreadex (
                ( (unsigned (__CLR_OR_STD_CALL *)(void *))(((_ptiddata)ptd)->_initaddr) )
                ( ((_ptiddata)ptd)->_initarg ) ) ;
    }
    __except ( _XcptFilter(GetExceptionCode(), GetExceptionInformation()) )
    {
            /*
                * Should never reach here
                */
            _exit( GetExceptionCode() );

    } /* end of _try - _except */

}
可以看到该函数的原型很像RtlUserThreadStart

关于_threadstartex有几个重点

新线程首先执行RtlUserThreadStart然后再跳转到_threadstartex

_threadstartex唯一的参数就是线程的_tiddata的内存块地址

TlsSetValue是一个操作系统函数,他将一个值和主调线程关联。(TLS) 这里 _threadstartex和_tiddata内存块关联。在

_callthreadstartex有一个SEH帧它将预期要执行的线程函数包围起来。这个帧处理这与运行库相关的许多事(异常,signal函数)

调用预期被执行的线程函数 

_endthreadex (
                ( (unsigned (__CLR_OR_STD_CALL *)(void *))(((_ptiddata)ptd)->_initaddr) )
                ( ((_ptiddata)ptd)->_initarg ) ) ;
线程函数的返回值被认为是线程的退出代码。

void __cdecl _endthreadex (
        unsigned retcode
        )
{
        _ptiddata ptd;           /* pointer to thread's _tiddata struct */
         HANDLE handle = NULL;

        ptd = _getptd_noexit();

        if (ptd) {
            if (ptd->_initapartment)
                _uninitMTAoncurrentthread();

            /*
             * Free up the _tiddata structure & its subordinate buffers
             *      _freeptd() will also clear the value for this thread
             *      of the FLS variable __flsindex.
             */
            _freeptd(ptd);
        }

        /*
         * Terminate the thread
         */
        ExitThread(retcode);

}

关于_endthreadex函数的要点

1)_getptd_noexit在内部调用TlsGetValue获得主调线程的tiddata内存块地址。

2)_endthreadex将此数据块释放。并调用操作系统的ExitThread函数实际销毁。

接下来所有C++运行库中需要与线程相关的函数都可以通过TlsGetValue来获得与其相关的线程数据_tiddata进行相关的处理。

errno全局变量是如何实现TLS呢?

errno在标准库被定义为一个宏 实际是调用一个_errno()的函数

_CRTIMP extern int * __cdecl _errno(void);
#define errno   (*_errno())

int * __cdecl _errno(
        void
        )
{
    _ptiddata ptd = _getptd_noexit();
    if (!ptd) {
        return &ErrnoNoMem;
    } else {
        return ( &ptd->_terrno );
    }

}

注意一个细节  _errno()函数返回一个指向内部pdt->_terno的值的指针。然后调用取值符号最后写成

(*_errno())这样写是很有必要的。

因为在实际代码中可能会这样使用errno  

例如 

int *p = &errno;

if(*p == ENOMEM)

这些代码实际等价于

int *p = &(*_errno(0);

是可以通过编译的。


c++运行库还维护了同步对象,例如两个线程同时调用malloc堆就会损坏。需要进行线程同步控制访问。


6.7.1 用_beginthreadex而不要用CreateThread创建线程

如果在CreateThread创建的线程中调用C++标准库函数会发生什么呢?

C++运行库会通过TlsGetValue尝试获取_tiddata块,返回NULL。 这时C++库函数会为主调线程初始化并分配一个_tiddata块。

然后这个块会与线程关联。以后运行的任何C++库函数都可以使用这个块。

但是!若线程使用了signal函数,整个进程都会终止,因为没有准备SHE帧。

另外如果不是通过_endthreadex来终止线程,数据块就无法被正常销毁,从而导致内存泄漏。

(说明 在标准C++的DLL库中如果线程终止会收到一个DLL_THREAD_DETACH通知,会释放线程相关的_tiddata块。但还是建议用_beginthreadex来创建线程,而不要用CreateThread)


6.7.2 绝对不应该调用的C/C++运行库函数。

_CRTIMP uintptr_t __cdecl _beginthread (
        void (__cdecl * initialcode) (void *),
        unsigned stacksize,
        void * argument
        );

这个函数比较老,不能创建具有安全属性的线程,不能创建让线程立即挂起,也不能获得线程ID值。

_endthread也类似,他是无参数的。

_endthread会在调用ExitThread前,调用CloseHandle。 然后此时hThread可能已经无效了会调用失败。

新的_endthreadex不会关闭线程的句柄。


6.8 了解自己的身份

windows提供了一些函数来方便线程引用他自己的进程内核对象或者他的线程内核对象。

HANDLE GetCurrentProcess();

HANDLE GetCurrentThread();

他们返回的是主调线程的进程内核对象的伪句柄。(不会在主调进程的句柄表中创建新的句柄,而且不会影响实际内核对象的引用计数器。所以也不需要CloseHandle)

应用

一个线程可以查询其所在进程的使用时间

	FILETIME ftCreateionTime, ftExitTime, ftKernelTime, ftUserTime;
	GetProcessTimes(GetCurrentProcess(),
		&ftCreateionTime, &ftExitTime, &ftKernelTime, &ftUserTime);

类似的也有GetThreadTimes一个线程可以查询自己的线程时间。

	FILETIME ftCreateionTime, ftExitTime, ftKernelTime, ftUserTime;
	GetThreadTimes(GetCurrentThread(),
		&ftCreateionTime, &ftExitTime, &ftKernelTime, &ftUserTime);

一下函数能获得进程或线程的操作系统级别的唯一ID

DWORD GetCurrentProcessId();

DWORD GetCurrentThreadId();


将伪句柄转换为真实句柄

有时候确实需要真实的句柄值。可以采用DuplicateHandle转换为真实句柄。

例如父线程创建子线程并传递自己的线程句柄被子线程调用。

一个错误传递线程句柄的例子

DWORD WINAPI ChildThread(PVOID pvParam) {
	HANDLE hThreadParent = (HANDLE)pvParam;
	FILETIME ftCreateionTime, ftExitTime, ftKernelTime, ftUserTime;
	GetThreadTimes(hThreadParent,
		&ftCreateionTime, &ftExitTime, &ftKernelTime, &ftUserTime);
	//..
	return 0;
}

DWORD WINAPI ParentThread(PVOID pvParam) {
	HANDLE hThreadParent = GetCurrentThread();
	CreateThread(NULL, 0, ChildThread, (PVOID)hThreadParent, 0, NULL);
	//...

	return 0;
}

采用 DuplicateHandle修改后的父线程代码


DWORD WINAPI ChildThread(PVOID pvParam) {
	HANDLE hThreadParent = (HANDLE)pvParam;
	FILETIME ftCreateionTime, ftExitTime, ftKernelTime, ftUserTime;
	GetThreadTimes(hThreadParent,
		&ftCreateionTime, &ftExitTime, &ftKernelTime, &ftUserTime);
	//..
	CloseHandle(hThreadParent);
	return 0;
}

DWORD WINAPI ParentThread(PVOID pvParam) {
	HANDLE hThreadParent;

	DuplicateHandle(
		GetCurrentProcess,		// Handle of process that thread
								// pseudohandle is relative to
		GetCurrentThread(),		// Parent thread's pseudohandle
		
		GetCurrentProcess(),	// Handle of process that the new, real,
								// thread is relative to
		&hThreadParent,			// Will receive the new, real, handle
								// identifying the parent thread
		0,						// Ignored due to DUPLICATE_SAME_ACCESS
		FALSE,					// New thread handle is not inheritable
		DUPLICATE_SAME_ACCESS	// New thread handle has same access as pseudohandle,
		);

	CreateThread(NULL, 0, ChildThread, (PVOID)hThreadParent, 0, NULL);
	//...

	return 0;
}

这样就会复制了一个真正的内核对象的句柄的记录,并且递增实际内核对象数据结构的引用计数器。

在子线程使用完次内核对象句柄,需要调用CloseHandle。

DuplicateHandle也可以把进程的伪句柄转换成真实的句柄对象。

	HANDLE hProcess;

	DuplicateHandle(
		GetCurrentProcess,		// Handle of process that thread
								// pseudohandle is relative to
		GetCurrentProcess(),	// Process's pseudohandle
		
		GetCurrentProcess(),	// Handle of process that the new, real,
								// thread is relative to
		&hProcess,			// Will receive the new, real, handle
								// identifying the parent thread
		0,						// Ignored due to DUPLICATE_SAME_ACCESS
		FALSE,					// New thread handle is not inheritable
		DUPLICATE_SAME_ACCESS	// New thread handle has same access as pseudohandle,
		);




  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值