《Windows核心编程》第6章 线程基础

来源:互联网 发布:台达plc编程软件 编辑:程序博客网 时间:2024/05/17 12:04

线程有两个部分组成:
1)线程内核对象。操作系统用它管理线程,它也是系统用来存放线程统计信息的地方。
2)线程栈。用于维护线程执行时所需的所有函数参数和局部变量。

每个线程都必须有一个入口点函数,这是线程执行的起点。如果想在进程中创建辅助线程,它必须有自己的入口点函数,形式如下:
DWORD WINAPI ThreadFunc(PVOID pvParam){
    DWORD dwResult = 0;
    …
    Return dwResult;
}
线程函数最终将终止运行并返回。此时线程将终止运行,用于线程栈的内存也会被释放,线程内核对象的使用计数也会递减。如果使用计数变为0,线程内核对象就会被销毁。

线程函数的名称可以任意,只有一个参数,必须有一个返回值。该返回值会成为线程的退出代码。

可以调用CreateThread函数来创建一个线程。此时系统会创建一个线程内核对象,这个线程内核对象不是线程本身,而是一个较小的数据结构,操作系统用这个结构来管理线程。该函数是用于创建线程的Windows函数。如果写的是C/C++代码,就不应该使用它,而要使用Microsoft C++运行库函数_beginthreadex。

CreateThread函数的参数cbStackSize指定线程可以为其线程栈使用多少地址空间。如果传入非0值,函数会为线程栈预定空间并为之调拨所需的所有存储空间。预留空间大小要么有/STACK链接器开关指定,要么由参数cbStackSize的值来指定,取其中较大的一个。预定的地址空间的容量设定了栈空间的上限。通过设置栈空间的上限,不仅可以防止应用程序耗尽物理内存区域,而且可以尽早觉察到程序中的BUG。

线程终止运行的四种方法:
1)    线程函数返回(强烈推荐)
2)    线程通过调用ExitThread函数“杀死”自己(避免使用)
3)    同一个进程或另一个进程中的线程调用TerminateThread函数(避免使用)
4)    包含线程的进程终止运行(避免使用)

通过让线程函数返回的方式,可以确保以下正确的应用程序清理工作得以执行:
● 线程函数中创建的所有C++对象都通过其析构函数被正确销毁
● 操作系统正确释放线程栈使用的内存
● 操作系统把线程的退出代码(在线程内核对象中维护)设为线程函数的返回值
● 系统递减线程内核对象的使用计数

要强迫线程终止运行,可以让它调用ExitThread。该函数将终止线程运行,并导致操作系统清理该线程使用的所有操作系统资源。但是,线程中创建的C/C++资源不会被销毁。要注意的是,ExitThread函数是用于杀死线程的Windows函数。如果要写的是C/C++代码,则需要使用C++运行库函数_endthreadex。

ExitThread是杀死主调线程,TerminateThread能杀死任何线程。TerminateThread函数是异步的,它告诉系统你想终止某个线程,但在函数返回时,并不保证线程已经终止了。如果需要确定线程已经终止运行,还需要调用WaitForSingleObject或类似函数,并向其传递线程的句柄。

函数ExitProcess和TerminateProcess也可以终止线程的运行。它们会使终止运行的进程中的所有线程全部终止。

线程终止运行时会发生的事情:
● 线程拥有的所有用户对象句柄会被释放。一般就是线程拥有的两个用户对象:窗口和挂钩。
● 线程的退出代码从STILL_ACTIVE变成传给ExitThread或TerminateThread的代码。
● 线程内核对象的状态变成触发状态。
● 如果线程是进程中最后一个活动线程,系统认为进程也终止了。
● 线程内核对象的使用计数递减1。
注意:线程终止运行时,其关联的线程内核对象不会自动释放,除非对这个对象的所有引用都被关闭了。

其他线程可以调用GetExitCodeThread来检查其参数hThread所标识的那个线程是否已经终止运行。如果终止运行,可判断其退出代码是什么。

对于_beginthreadex函数,需要关注的地方:
● 每个线程都有自己的专用_tiddata内存块,它们是从C/C++运行库的堆上分配的。
● 传给_beginthreadex的线程函数的地址保存在_tiddata内存块中。
● _beginthreadex在函数内部会调用CreateThread,因为操作系统只知道用这种方式来创建一个新线程。
● CreateThread函数被调用时,传给它的函数地址是_threadstartex,而不是pfnStartAddr。另外,参数地址是_tiddata结构的地   址,而非pvParam。
● 如果一切顺利,会返回线程的句柄。如果任何操作失败,会返回0。

对于_threadstartex函数,要注意以下几点:
● 新的线程首先执行RtlUserThreadStart,然后再跳转到_threadstartex。
● _threadstartex唯一参数就是新线程的_tiddata内存块的地址。
● TlsSetValue是一个操作系统函数,它将一个值与主调线程关联起来。
● 在无参数的辅助函数_callthreadstartex中,有一个SHE帧,它将预期要执行的线程函数包围起来。
● 预期要执行的线程函数会被调用,并向其传递预期的参数。
● 线程函数的返回值被认为是线程的退出代码。

对于_endthreadex函数,需要注意以下几点:
● C运行库的_getptd_noexit函数在内部调用操作系统的TlsGetValue函数,后者获取主调线程的tiddata内存块的地址。
● _endthreadex将此数据块释放,并调用操作系统的ExitThread函数来实际销毁线程。

C/C++运行库对于一些特定的函数放置了同步对象。例如,如果两个线程同时调用malloc,堆就会被破坏。C/C++运行库函数不允许两个线程同时从内存堆中分配内存。具体做法是让第2个线程等待,直到第1个线程从malloc函数返回。然后才允许第2个线程进入。当然,这些额外的工作会影响多线程版本的C/C++运行库的性能。

如果调用CreateThread而不是C/C++运行库的_beginthreadex来创建新线程时,会发生(比如当一个线程调用一个需要_tiddata结构的C/C++运行库函数时)的情况:C/C++运行库函数尝试取得线程数据块的地址(即通过调用TlsGetValue得到_tiddata的地址)。如果NULL被作为_tiddata块的地址返回,表明主调线程没有与之关联的_tiddata块。此时,C/C++运行库函数会为主调线程分配并初始化一个_tiddata块。然后将这个块与线程关联(通过调用TlsSetValue),而且只要线程还在运行,这个块就会一直存在并与线程关联。这样C/C++运行库函数可以使用线程的_tiddata块,以后调用的任何C/C++运行库函数也都可以使用。

线程可以通过调用Windows提供的函数GetCurrentProcess和GetCurrentThread来引用它的进程内核对象或它自己的线程内核对象。这两个函数返回的都只是相应内核对象的伪句柄。它们不会在主调进程的句柄表中新建句柄,而且调用这两个函数,也不会影响相应内核对象的使用计数。

调用一个Windows函数时,如果此函数需要一个进程句柄或线程句柄,那么可以传递一个伪句柄,这将导致函数在主调进程或线程上执行它的操作。

线程可以通过调用GetCurrentProcessId或GetCurrentThreadId来得到其所在进程或自身线程的唯一ID。

“真正的线程句柄”,指的是能明确、无歧义地标识一个线程内核对象的句柄。线程的伪句柄是一个指向当前线程的句柄,即指向的是发出函数调用的那个线程。

在使用很多函数的时候,我们都需要获得一个对象的句柄,而某些函数返回的是伪句柄。伪句柄本身不会打开内核对象的句柄表,因此内核对象的使用计数就不会增加。它本身就只指向调用它的主调进程或线程,会因为调用者的不同而改变。比如:调用者A使用一个伪句柄,这个句柄指向调用者A,而调用者A将该句柄传递给调用者X,则这个句柄就指向调用者X。我们可以通过调试的方式查看伪句柄,可以得知,进程的伪句柄总是0xffffffff,而线程的伪句柄总是 0xfffffffe。

通过使用DuplicateHandle这个强大的函数,可以将伪句柄转换为真正的句柄。使用DuplicateHandle函数,我们可以根据与进程A相关的一个内核对象句柄来创建一个新句柄,并让它同进程B相关。例如把线程的伪句柄转换为真正的线程句柄:
HANDLE hThread;
DuplicateHandle(
            GetCurrentProcess(),
            GetCurrentThread(),
            GetCurrentProcess(),
            &hThread,
            0,
            FALSE,
            DUPLICATE_SAME_ACCESS);
把进程的伪句柄转换为真正的进程句柄:
HANDLE hProcess;
DuplicateHandle(
            GetCurrentProcess(),
            GetCurrentProcess(),
            GetCurrentProcess(),
            &hProcess,
            0,
            FALSE,
            DUPLICATE_SAME_ACCESS);

原创粉丝点击