前言:
线程也有两部分组成:
a.一个是线程的内核对象,操作系统用它来管理线程。系统还用内核对象来存放线程统计信息的地方
b.一个线程栈,用于维护线程执行时所需的所有函数参数和局部变量
线程必然是在某个进程的上下文创建的,而且会在这个进程内部”终其一生“。这意味着线程要在其进程的地址空间内执行代码和处理数据。所以假如一个进程上下文中有两个以上线程运行,这些线程将共享同一个地址空间。这些线程可以执行同样的代码,可以处理相同的数据。此外,这些线程还共享内核对象句柄,因为句柄表是针对每个进程的,而不是针对每一个线程。
6.1 何时创建线程
线程描述了进程内部的一条执行线路。每次初始化进程时,系统都会创建一个主线程。对于用Microsoft C/C++编译器生产的应用程序,这个线程首先会执行C/C++运行库的启动代码,后者调用入口点函数(_tmain或_tWinMain),并继续执行,直至入口点函数返回C/C++运行库的启动代码,后者最终将调用ExitProcess。
让CPU尽量不空闲,让其尽量保持忙碌状态。多线程简化了应用程序的用户界面的设计,也可以使应用程序更容易扩展。
6.2 何时不应该创建线程
在几乎所有的应用程序中,所有用户界面组件(窗口)都应该共享一个线程。一个窗口的所有子窗口无疑应该由同一个线程来创建。并且一般情况下用户界面线程的优先级要高于工作线程,这才能保证用户界面才能迅速响应用户的操作。
6.3 编写第一个线程函数
每个线程函数都必须有个一个入口点函数,这是线程执行的起点。
6.3 CreateThread函数
调用该函数时,系统会创建一个线程内核对象。这个线程内核对象不是线程本身,而是一个较小的数据结构,操作系统用这个结构来管理线程。可以把线程内核对象想象为一个由线程统计信息构成的小型数据结构。
系统从进程的地址空间中分配内存给线程栈使用。新线程在与负责创建的那个线程在相同的进程上下文中运行。因此新线程可以访问进程内核对象的所有句柄、进程中的所有内存以及同一个进程中的其他线程的栈。这样一来,同一个进程中的多个线程可以很容易地互相通信。
Windows是一个抢占式的多线程系统。这意味着新的线程和调用CreateThread函数的线程可以同时执行。因为两个线程可以同时运行,所以有可能会出现问题。
如果写的是C/C++代码,就绝对不要调用CreateThread,正确的选择是使用Microsoft C+运行库函数_beginthreadex。
6.4.1 psa参数
该参数是一个安全属性的指针。如果希望所有的子进程都能继承到这个线程对象的句柄,必须指定一个SECURITY_ATTRIBUTES结构,并将该结构的bInheritHandle成员初始化为true。向该参数传入NULL则是使用线程内核对象的默认安全属性。
6.4.2 cbStackSize
该参数指定线程可以为其线程栈使用多少地址空间。每个线程都有自己的栈。
6.4.3 pfnStartAddr和pvParam参数
pfnStartAddr参数指定希望新线程执行的线程函数的地址。pvParam参数与最初传给CreateThread函数的pvParam参数是一样的。CreateThread不用这个参数做别的事情,只是在线程开始时将其传给线程函数。通过这个参数,可以将一个初始值传给线程函数。创建多个线程时,可以让它们使用同一个函数地址作为起点。
Windows是一个抢占式的多线程系统,这意味着新的线程和调用CreateThread函数的线程可以同时执行。
6.4.4 dwCreateFlags
该参数指定额外的标志来控制线程的创建。0表示线程创建后立即就可以进行调度;如果值为CREATE_SUSPENDED,系统将创建并初始化线程,但是会暂停该线程的执行,这样它就无法进行调度。
6.4.5 pdwThreadID
CreateThread用该参数来存储系统分配给新线程的ID。
6.5 终止运行线程
线程可以通过以下四种方法来终止运行:
1. 线程函数返回(这是强烈推荐的)
2. 线程通过调用ExitThread函数“杀死”自己(要避免使用这种方法)
3. 同一个进程或另一个进程中的线程调用TerminateThread函数(要避免使用这种方法)
4. 包含线程的进程终止运行(这种方法避免使用)
6.5.1 线程函数返回
设计线程函数时,应该确保在我们希望线程终止运行时,就让它们返回。这是保证线程所有资源都被正确清理的唯一方式。让线程函数返回,可以确保一下正确的应用程序清理工作都得以执行。
a.线程函数中创建的所有C/C++对象都通过其析构函数被正确销毁
b.操作系统正确释放线程栈使用的内存
c.操作系统把线程的退出代码(在线程内核对象中维护)设为线程函数的返回值
d.系统递减线程的内核对象的使用计数
6.5.2 ExitThread
该函数将终止线程的运行,并导致操作系统清理该线程使用的所有操作系统资源,该线程的线程栈也会被销毁,但是用户的C/C++资源不会被销毁。该函数没有返回值,因为线程已终止,而且不会执行更多的代码。C/C++中该函数的替换函数为_endthreadex。
6.5.3 TerminateThread函数(异步的)
不同于ExitThread总是杀死主调线程,TerminateThread可以杀死任何线程,线程终止时,其退出代码将变成你作为dwExitCode参数传递的值,同时线程的内核对象的使用计数会递减。如果使用该函数,那么除非拥有此线程的进程终止运行,否则系统不会销毁这个线程的堆栈。
6.5.4 进程终止运行时
6.5.5 线程终止运行时
线程终止运行时会发生下面这些事情:
①线程拥有的所有用户对象句柄会被释放。在Windows中,大多数对象都是由包含了“创建这些对象的线程”的进程拥有的,但一个线程由两个用户对象:窗口和挂钩。一个线程终止运行时,系统会自动销毁由线程创建或安装的如何窗口,并卸载由线程创建或安装的任何挂钩。其他对象只有在拥有线程的进程终止时才被销毁。
②线程的退出代码从STILL_ACTIVE变成传给ExitThread或TerminateThread的代码
③线程内核对象的状态变为触发状态
④如果线程是进程中的最后一个活动线程,系统就认为进程也终止了
⑤线程内核对象的使用计数递减1
线程终止运行时,其关联的线程对象不会被自动释放,除非对这个对象的所有未结束的引用都被关闭了。
6.6 线程内幕
因为新线程的指令指针被设为RtlUserThreadStart,所以这个函数实际上就是线程开始执行的地方。
用ExitProcess。所以对应C/C++应用程序来说,主线程永远不会返回到RtlUserThreadStart函数。
6.7 C/C++运行库注意事项
C/C++应用程序应该调用_beginthreadex,而不应该调用CreateThread来创建一个线程。
6.7.1 用_beginthreadex而不要用CreateThread创建线程
_beginthreadex在threadex.c文件中,位于C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\crt\src下。
6.8 了解自己的身份
伪句柄:GetCurrentProcess和GetCurrentThread这两个函数都返回到主调线程的进程内核对象或线程内核对象的一个伪句柄,它们不会在主调进程的句柄表中新建句柄,而且调用这两个函数,不会影响进程内核对象或线程内核对象的使用计数。如果调用CloseHandle,将一个伪句柄作为参数传入,CloseHandle只是简单地忽略此调用,并返回false。
真正句柄:有几个Windows函数可以让我们用进程或线程唯一的系统级ID来标识它,如GetCurrentProcessId和GetCurrentThreadId。
线程的伪句柄是一个指向当前线程的句柄,换言之,指向的是发出函数调用的那个线程。
可以通过DuplicateHandle函数来将伪句柄转换成真正的句柄。由于DuplicateHandle递增了指定内核对象的使用计数,所以在用完复制的对象句柄后,有必要把目标句柄传给CloseHandle,以递减对象的使用计数。