第四章:线程
上面提到过,进程有两部分构成:一个是进程内核对象,一个是地址空间。同样,线程也由两部分构成:
1.线程内核对象。
2.线程的堆栈。
进程的特性是不活泼的,它从来不执行任何代码。进程只是线程的一个容器。且线程总是在某个进程的环境中产生的,而且它的整个寿命都在该进程中。要注意到这点关系。
在进程中,两个或多个线程可以:共享进程的地址空间,执行相同的代码,对相同的数据进行操作,共享进程中的内核对象句柄。
主线程:进程启动时,主线程即开始运行,它的寿命期是从调用进入点函数WinMain或Main开始,直到函数返回C/C++运行期库调用ExitProcess函数止。
一,编写线程函数:
函数代码如下所示:
DWORD WINAPI ThreadFunc(PVOID pvParam)
{
DWORD dwResult = 0;
return(dwResult);
}
可以在....中执行你想要的任务。
关于线程的说明:
1.主线程的名字必须是main或WinMain,而非其它,不可自定义主线程函数。
2.线程必须返回一个值,以作为线程的退出代码,主线程也不例外。
3.线程函数尽可能使用局部变量或参数,如果使用静态变量或全局变量,则进程中的其它进程可以访问并修改这些变量值。
二,创建一个线程内核对象:
函数原型如下:
HANDLE CreateThread(
PSECURITY_ATTRIBUTES psa,
DWORD cbStack,
PTHREAD_START_ROUTINE pfnStartAddr,
PVOID pvParam,
DWORD fdwCreate,
PDWORD pdwThreadID);
当调用CreateThread函数时,系统将创建一个线程的内核对象,注意:线程内核对象不是线程本身,而是操作系统用来管理线程的数据结构。可以将线程内核对象视为由关于线程统计信息组成的一个小型数据结构。
然后,系统从进程的地址空间中分配内存,供线程堆栈使用。线程可以访问进程中所有内核对象的句柄,进程中所有的内存,其它线程的堆栈。使得单个进程中的多个线程的通信非常容易。
注:CreateThread是Windows API的函数,如果使用C/C++写多线程,不应该使用CreateThread函数,而应该使用C++运行库函数_beginthreadex函数。
参数介绍:
1.psa:无需介绍,每个内核对象都有,描述安全性的结构。
2.cbStack:设定线程空间可以将多少地址空间用于它的堆栈。
3.pfnStartAddr:设定新线程执行的线程函数的地址。
4.pvParam:线程函数用的参数。
5.fdwCreate:设定用于控制线程创建的参数,设定为0,则表示立即创建该线程,设定为CREATE_SUSPENDED,则表示程序能够在它有机会执行任何代码之前修改线程的某些属性,这个值并不常用。
6.pdwThreadID:一个地址,用来存放系统分配给新线程的ID。
三,终止线程运行
1.线程函数的返回(推荐)
2.通过调用ExitThread函数,线程将自动撤销。
3.调用TerminateThread函数。
4.包含线程的进程终止运行。
ExitThread能够使本线程立即终止运行,但是不何证C++资源被正确释放。而TerminateThread能够使任何线程终止,只要给它的参数传入线程的句柄即可。这一点根进程的ExitProcess与TerminateProcess函数是相似的。
TerminateThread函数调用后,不保证线程已经被撤销了,因为该函数是异步函数,如果需要知道是否被撤销,需要使用WaitForSingleObject函数。
应该避免使用2,3,4种方法来结束线程。
四,线程的一些性质
1.一旦线程内核对象建立完成,系统就给线程从进程的地址空间中分配堆栈。
2.每个线程都有它自己的CPU寄存器,称为线程的上下文。该上下文保存了上次线程运行时保存的CPU寄存器的状态。
3.指令指针与堆栈指针是线程上下文中最重要的寄存器,注意:线程总是在上下文中运行的。
五,对自己的ID
的获得
获得本进程与本线程的ID可以用函数:
DWORD GetCurrentProcessId();
DWORD GetCurrentThreadId();
但是,这两个函数是获得ID的伪句柄,我们可以获得某进程或线程的唯一句柄值 ,那就是用DuplicateHandle函数:
BOOL DuplicateHandle(
HANDLE hSourceProcess,
HANDLE hSource,
HANDLE hTargetProcess,
PHANDLE phTarget,
DWORD fdwAccess,
BOOL bInheritHandle,
DWORD fdwOptions);
具体可参考书本第六章。
六,线程的调度
Windows是抢占式操作系统,所以,系统可以对线程进行何时调度,调度时间是多少。上面说,每个线程都有上下文,记录着上次该线程上次在CPU运行时寄存器的状态。每隔20ms,系统都会查看所有的线程内核对象,在这些对象中,只有某些可以被视为可调度对象。系统选择其一,调度进CPU中。然后,线程把保存的上下文加载到各寄存器,继续运行线程末完成的代码,再过20ms,如果线程没有完成,则把寄存器的值存进线程的上下文中,挂起线程,选择另外的可调度线程。系统一直重复这样的动作直到关闭系统。
Windows即是抢占式,表示某线程可以随时终止,某可调度线程可以随时被调度。程序可以进行一定的控制调度。
Windows中很多线程都是不可调度线程,为什么这么说?因为很多线程都是在等待某个事件的发生。比如,运行Notepad记事本程序,但是,什么也不键入,那么线程调度起来也没用,只有在Notepad中键入值或移动notepad窗口等操作,它才可变为可调度程序。还有,有些线程的暂停计数大于1,表示线程已经暂停,也不能调度,通过调用CREATE_SUSPENED标志的CreateProcess或CreateThread函数,可以创建暂停线程。
七,暂停线程
线程的内核对象中有一个值,指明了线程的暂停计数。可以多次对线程进行暂停操作,也可多次对线程进行恢复操作。暂停中的线程,不参与CPU的调度。
在上面说过,可以在CreateProcess或CreateThread中创建暂停线程,另外,也可以对创建好的线程暂停,用SuspendThread函数来操作:
DWORD SuspendThread(HANDLE hThread);
恢复线程则用ResumeThread函数:
DWORD ResumeThread(HANDLE hThread);
注:任何线程都可以通过调用SuspendThread来暂停其它线程,则要拥有其它线程的ID。线程可以自动暂停运行,但是,不可以自已恢复运行。
八,线程的睡眠
如果不想让某个线程在某段时间内被调度,则可以使线程处于睡眠状态。
VOID Sleep(DWORD dwMilliseconds);
九,转换线程
系统提供SwitchThread函数,使得另一个线程可调度,调用这个函数,系统查看是否有一个需要CPU时间片的线程,如果有,则对该线程进行调度,如果没有,则返回FALSE。
函数:
BOOL SwitchToThread();
十,返回线程的运行时间
BOOL GetThreadTimes(HANDLE hThread,
PFILETIME pftCreationTime, PFILETIME pftExitTime,
PFILETIME pftKernelTime, PFILETIME pftUserTime);
返回四个值,分别是:创建时间,退出时间,内核时间,用户时间
注:GetProcessTimes是类似于它的函数,适用于进程中的所有线程:
BOOL GetProcessTimes(HANDLE hProcess,
PFILETIME pftCreationTime, PFILETIME pftExitTime,
PFILETIME pftKernelTime, PFILETIME pftUserTime);