Windows核心编程【6】小结

来源:互联网 发布:office2003是什么软件 编辑:程序博客网 时间:2024/06/02 18:24

第六章 线程基础


1、每个进程至少都有一个线程。
2、线程的两个组件:一个是线程的内核对象,操作系统用它管理线程;另一个是线程堆栈,用于维护线程执行时所需的所有函数参数和局部变量。
3、进程是有惰性的,它从来不执行任何东西,只是一个线程的容器。
4、线程只有一个内核对象和一个堆栈,几乎不涉及记录活动,不需要占用多少内存。


一、何时创建线程

1、线程描述了进程内部的一个执行路径。每次初始化进程时,系统都会创建一个主线程。对于MS的C/C++编译器生成的应用程序,该线程会先执行C/C++运行库的启动代码,启动代码调用入口函数,继续执行,直至入口函数返回C/C++运行库的启动代码,后者最终调用ExitProcess。对许多应用程序来说,这个主线程是应用程序唯一需要的线程。
2、忽略省电和发热问题,让CPU空闲着是没有任何道理的。
3、将一个应用程序设计成多线程的,可以使该应用程序更易于扩展。


二、何时不应该创建线程

1、通常应用程序应该有一个用户界面线程,此线程负责创建所有窗口,另外还有一个GetMessage循环。进程中的其它所有线程都是受计算机制约或者I/O限制的工作线程,这些线程永远不会创建窗口。另外,用户界面线程的优先级通常高于工作线程。这样用户界面才能灵敏地响应用户的操作。
2、Windows资源管理器就为每个文件夹的窗口创建了一个独立的线程。这样一来,就可以把文件从一个文件夹复制到另一个文件夹,同时仍然可以查看系统上的其它文件夹。另外,如果Windows资源管理器出现一个bug,正在操作的一个文件夹的线程就可能崩溃。
3、应该合理使用多线程,不要因为一件事情是你能做的就非要去做。


三、编写第一个线程函数

1、每个线程都必须有一个入口函数,这是线程执行的起点。主线程的入口函数:_tmain或_tWinMain。如果想在进程中创建辅助线程,它必须有自己的入口函数,形式如下,DWORD WINAPI ThreadFunc(PVOID pvParam)。
2、线程终止运行,其堆栈的内存也会被释放,线程内核对象的使用计数也会递减。如果使用计数变为0,线程内核对象会被销毁。类似于进程内核对象。
3、主线程的入口函数默认必定命名为main,wmain,WinMain或wWinMain(有一种情况除外,也就是使用/ENTRY:链接器选项来指定另一个函数作为入口函数)。而线程函数可以任意命名,不过需要指定不同的名称,否则编译器/链接器会认为你创建了一个函数的多个链接。
4、由于向主线程传递了字符串参数,所以可以使用入口函数的ANSI/Unicode版本。相反,向线程函数传递的是一个意义由我们而非操作系统来定义的参数。因为,不必担心ANSI/Unicode问题。
5、线程函数必须返回一个值,它会成为该线程的退出代码。
6、线程函数应该尽可能使用函数参数和局部变量。使用静态和全局变量时,多个线程可以同时访问这些变量,这样可能会破坏变量中保存的内容。


四、CreateThread函数

1、调用CreateThread函数
HANDLE WINAPI CreateThread(
  __in_opt   LPSECURITY_ATTRIBUTES lpThreadAttributes,
  __in       SIZE_T dwStackSize,
  __in       LPTHREAD_START_ROUTINE lpStartAddress,
  __in_opt   LPVOID lpParameter,
  __in       DWORD dwCreationFlags,
  __out_opt  LPDWORD lpThreadId
);
2、调用CreateThread时,系统会创建一个线程内核对象,并将进程地址空间的内存分配给线程堆栈使用。新线程在与负责创建的那个线程相同的进程上下文中运行。因此,新线程可以访问进程内核对象的所有句柄、进程中的所有内存以及同一个进程中其他所有线程的堆栈。这样一来,同一个进程中的多个线程就可以很容易地相互通信。
3、lpThreadAttributes参数指向SECURITY_ATTRIBUTES结构,这个在第三章介绍了。
4、dwStackSize参数指定线程可以为其线程堆栈使用多少地址空间。每个线程都拥有自己的堆栈,当CreateProcess函数开始一个进程的时候,它会在内部调用CreateThread来初始化进程的主线程。对于dwStackSize参数,CreateProcess使用了保存在可执行文本内部的一个值。可以使用链接器/STACK开关来控制这个值,如下所示:
/STACK:[reserve] [,commit]
reserve参数用于设置系统将为线程堆栈预留多少地址空间,默认是1MB。commit参数指定最初应为堆栈的保留区域提交多少物理存储空间,默认是1页。随着线程中的代码开始执行,需要的存储空间可能不止1页。如果线程溢出它的堆栈,会产生异常。系统将捕获这种异常,并将另一个页(或者为commit参数指定的任何大小)提交给保留空间。这样一来,线程堆栈就可以根据需要动态地增大了。调用CreateThread时,如果传入非0值,函数会保留并提交(commit)线程堆栈的所有存储。如果两者都指定了,则取其中较大的那一个。
5、lpStartAddress参数指定希望新线程执行的线程函数的地址。lpParameter参数与最初传给CreateThread函数的lpParameter参数是一样的。通过这个参数,可以讲一个初始值传给线程函数。这个初始值可以是一个数值,也可以是指向一个数据结构(其中包含额外的信息)的指针。创建多个线程时,可以让他们使用同一个函数地址作为起点。这样做完全合法,而且非常有用。
6、由于Windows是一个抢占式的多线程系统(preemptive multithreading system),意味着新的线程和调用CreateThread函数的线程可以同时执行。所以可能出现传过去的lpParameter在原函数中被销毁,而新创建的线程尝试引用产生访问冲突。解决这个问题的一种方法是将变量声明为一个静态变量,是编译器能在应用程序的数据section(而不是线程堆栈)中为变量创建一个存储区域。但是,这会使函数不可重入。换言之,不能创建两个线程来执行同一个函数,因为这两个线程将共享同一个静态变量。为了解决这个问题,可以使用线程同步技术(八九章将讲述到)
7、dwCreationFlags参数指定额外的标志来控制线程的创建。他可以是三个值之一(第五版原书当时为两种)。见MSDNhttp://msdn.microsoft.com/en-us/library/windows/desktop/ms682453(v=vs.85).aspx。


五、终止运行线程

1、线程函数返回,强烈推荐的方式。这是保证线程的所有资源都被正确清理的唯一方式。线程函数中创建的所有C++对象都通过其析构函数被正确销毁;操作系统正确释放线程堆栈使用的内存;操作系统把线程的退出代码(在线程的内核对象中维护)设为线程函数的返回值;系统递减线程的内核对象的使用计数。
2、ExitThread函数,该函数将终止线程的运行,并导致操作系统清理该线程使用的所有操作系统资源。但是,我们的C/C++资源(如C++类对象)不会被销毁。如果是要写C/C++代码,就绝对不要调用ExitThread,应该使用C++运行库函数endthreadex。
3、TerminateThread函数,不同于ExitThread杀死主线程,TerminateThread能杀死任何线程。是异步的,会告诉系统要终止线程,但是在函数返回的时候,线程不能保存已经终止。一个设计良好的应用程序绝不会使用这个函数,因为被终止运行的线程收不到它被杀死的通知。线程无法正确清理,而且不能阻止自己被终止运行。同时要注意的是,使用TerminateThread,除非拥有此线程的进程终止运行,否则系统不会销毁这个线程的堆栈。MS故意以这种方式来实现TerminateThread。否则假如其他还在运行的线程要引用被杀死的那个线程的堆栈上的值,就会引起访问冲突。让被杀死的线程的堆栈保留在内存中,其他的线程就可以继续正常运行。
4、当进程终止运行时,第四章介绍的ExitProcess和TerminateProcess函数,相当于为剩下的每个线程都调用了TerminateThread。
5、当线程终止运行时,线程的退出代码从STILL_ACTIVE变为传给ExitThread或TerminateThread的代码;线程内核对象的状态变为signaled;如果线程是进程中的最后一个活动线程,系统认为进程也终止了;线程内核对象的使用计数递减1。


六、线程内幕

1、对CreateThread函数的一个调用导致系统创建一个线程内核对象。该对象最初的使用计数为2,暂停计数被设为1,退出代码被设为STILL_ACTIVE(0x103),而且对象被设为nonsignaled状态。如下图:


2、每个线程都有其自己的一组CPU寄存器,称为线程的上下文(context)。上下文反映了当线程上次执行时,线程的CPU寄存器的状态。线程的CPU寄存器全部保存在一个CONTEXT结构(在WinNT.h)。CONTEXT结构本身保存在线程内核对象中。
3、指令指针和栈指针寄存器是线程上下文中最重要的两个寄存器。这两个地址都标识了“拥有线程的那个进程”的地址空间的内存。当线程的内核对象被初始化的时候,CONTEXT结构的堆栈指针寄存器被设为pfnStartAddr在线程堆栈中的地址。而指令指针寄存器被设为RtlUserThreadStart函数。
4、线程完全初始化好之后,系统将检查CREATE_SUSPENDED标志是否传给CreateThread函数。如果此标志没有传递,系统将线程的暂停计数递减至0;随后,线程就可以调度给一个处理器去执行。
5、新线程执行RtlUserThreadStart函数的时候,将发生以下事情。围绕线程函数,会设置一个结构化异常处理(SEH)帧。这样一来,线程执行期间所产生的任何异常都能得到系统的默认处理。系统调用我们的线程函数,将传给CreateThread函数的lpParameter参数传给它。线程函数返回时,RtlUserThreadStart调用ExitThread。如果线程产生了一个未被处理的异常,RtlUserThreadStart函数所设置的SEH帧会处理这个异常。通常意味着向用户显示一个消息框,而且当用户关闭此消息框时,RtlUserThreadStart会调用ExitProcess来终止整个进程,而不只是终止有问题的线程。
6、在RtlUserThreadStart内,线程会调用ExitThread或者ExitProcess,这意味着线程永远不能退出此函数;它始终在其内部消亡。同时,RtlUserThreadStart函数是不允许返回的,因为线程堆栈上没有函数返回地址,所以几乎肯定会引起访问冲突。主线程永远不会返回到RtlUserThreadStart函数,因为在之前C/C++运行时启动代码会调用ExitProcess。


七、C/C++运行库注意事项

原创粉丝点击