第 8章 进程与线程
Win32操作系统平台提供了强大的多任务功能,其中
“进程”( Process)和“线程” (Thread)是其控制多任务的两个重要概念。早期的 Windows 3.x只能依靠应用程序之间的协同来实现协同式多任务,而 Windows 95/NT实行的是抢占式多任务。
在 Win 32(Windows 95/NT)中,每一个进程可以同时执行多个线程,这意味着一个程序可以同时完成多个任务。
对于象通信应用程序那样的既要进行耗时的工作,又要保持对用户输入响应的应用来说,使用多线程是最佳选择。
当进程使用多个线程时,需要采取适当的措施来保持线程间的同步。
进程与子进程进程( process)是计算机操作系统中的概念。进程可以理解为:程序在给定的初始状态和内存区域中,能共行执行的一次“计算”,
亦即,进程是可以和其它程序共行执行的,程序的一次执行。进程有独自的内存空间、程序代码、信息以及一堆大大小小的系统资源。
另外,进程之间也有父子关系,产生进程的进程是“父进程”,被产生的进程是“子进程”,通常父进程对子进程有控制权,同时子进程也可以存取父进程的资源。
创建子进程 API函数下面介绍一个很重要的函数,即 CreateProcess函数,其原型为:
BOOL CreateProcess(LPCTSTR lpApplicationName,LPTSTR
lpCommandLine,LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,BOOL bInheritHandles,
DWORD dwCreationFlags,LPVOID lpEnvironment,LPCTSTR
lpCurrentDirectory,LPSTARTURINFO lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation);
CreateProcess() 的功能是建立并执行 child process。
其它函数本节将介绍一些可以协助我们取得 process相关信息的函数。第一组要介绍的是最重要的 GetCurentProcess()GetCurrentProcessId(),前者会返回目前正在执行的 process(也就是调用者 )的 handle,后者会返回调用者的
id。 Handle和 id有什么不同呢? id只是一个数字,Win32保证不会有第二个在系统中执行的 process拥有相同的 id,因此,这个数字通常用来鉴别 process的身份; process handle可就重要了,因为其他与 process有关的函数都需要它来当参数。
l HANDLE GetCurrentProcess(VOID);
l DWORD GetCurrentProcessId(VOID);
DWORD GetPriorityClass(HANDLE hProcess);
BOOL SetPriorityClass(HANDLE hProcess,DWORD
dwPriorityClass);
结束 process
如果某个 process想停止执行,可调用 ExitProcess(),不过我们通常不直接调用它,而是调用 C程序库中的 exit(),
exit()在自动执行一些清除垃圾的工作之后,再调用
ExitProcess()。
VOID ExitProcess(UNT uExitCode);
不过,如果 process A 想要迫使 process B 停止执行,
可以在取得 process B 的 handle 之后,调用
TerminateProcess():
BOOL TerminateProcess(HANDLE hProcess,UNIT
uExitCode);
进程与线程前一节我们讨论了进程和子进程的有关概念和编程技术,实际上在 Windows 操作系统环境中,Microsoft提出了线程是更具有挑战性的编程概念。在 Windows 32位操作系统中,所谓多任务是指系统可以同时运行多个进程,而每个进程也可以同时执行多个线程。所谓进程就是应用程序的运行实例。一个进程又可以分为多个线程,
根据线程的运行特征,我们可以把它看成是操作系统分配 CPU时间的基本实体。
Win16 的协同多任务早在 16位的 Windows中,应用程序具有对 CPU的控制权。只有在调用了 GetMessage,PeekMessage,WaitMessage或 Yield后,程序才有可能把 CPU
控制权交给系统,系统再把控制权转交给别的应用程序。如果应用程序在长时间内无法调用上述四个函数之一,那么程序就一直独占 CPU,系统会被挂起而无法接受用户的输入。
有人可能会想到用 CWinApp::OnIdle函数来执行后台工作,因为该函数是程序主消息循环在空闲时调用的。但 OnIdle的执行并不可靠,例如,如果用户在程序中打开了一个菜单或模态对话框,那么 OnIdle将停止调用,因为此时程序不能返回到主消息循环中!在实时任务代码中调用 PeekMessage也会遇到同样的问题。
折衷的办法是在执行长期工作时弹出一个非模态对话框并禁止主窗口,在消息循环内分批执行后台操作。对话框中可以显示工作的进度,也可以包含一个取消按钮以让用户有机会中断一个长期的工作。典型的代码如程序
8.1所示。这样做既可以保证工作实时进行,又可以使程序能有限地响应用户输入,但此时程序实际上已不能再为用户干别的事情了。
Windows 95/NT的抢先式多任务在 32位的 Windows系统中,采用的是抢先式多任务,这意味着程序对 CPU的占用时间是由系统决定的。系统为每个程序分配一定的
CPU时间,当程序的运行超过规定时间后,系统就会中断该程序并把
CPU控制权转交给别的程序。与协同式多任务不同,这种中断是汇编语言级的。程序不必调用象 PeekMessage这样的函数来放弃对 CPU的控制权,就可以进行费时的工作,而且不会导致系统的挂起。
例如,在 Windows3.x 中,如果某一个应用程序陷入了死循环,那么整个系统都会瘫痪,这时唯一的解决办法就是重新启动机器。而在
Windows 95/NT中,一个程序的崩溃一般不会造成死机,其它程序仍然可以运行,用户可以按 Ctrl+Alt+Del键来打开任务列表并关闭没有响应的程序。
进程与线程所谓进程就是应用程序的运行实例。每个进程都有自己私有的虚拟地址空间。每个进程都有一个主线程,但可以建立另外的线程。进程中的线程是并行执行的,每个线程占用 CPU的时间由系统来划分。
进程中的所有线程共享进程的虚拟地址空间,这意味着所有线程都可以访问进程的全局变量和资源。这一方面为编程带来了方便,但另一方面也容易造成冲突。
虽然在进程中进行费时的工作不会导致系统的挂起,但这会导致进程本身的挂起。所以,如果进程既要进行长期的工作,又要响应用户的输入,
那么它可以启动一个线程来专门负责费时的工作,而主线程仍然可以与用户进行交互。
线程的创建和终止线程分用户界面线程和工作者线程两种。用户界面线程拥有自己的消息泵来处理界面消息,可以与用户进行交互。工作者线程没有消息泵,
一般用来完成后台工作。
MFC应用程序的线程由对象 CWinThread表示。在多数情况下,程序不需要自己创建 CWinThread对象。调用 AfxBeginThread函数时会自动创建一个 CWinThread对象。
当发生下列事件之一时,线程被终止:
l 线程调用 ExitThread;
l 线程函数返回,即线程隐含调用了 ExitThread;
l ExitProcess被进程的任一线程显式或隐含调用;
l 用线程的句柄调用 TerminateThread;
l 用进程句柄调用 TerminateProcess。
线程的同步多线程的使用会产生一些新的问题,主要是如何保证线程的同步执行。多线程应用程序需要使用同步对象和等待函数来实现同步。同步问题是实现远程数据采集或远程自动控制编程中的关键技术问题。
为什么需要同步由于同一进程的所有线程共享进程的虚拟地址空间,并且线程的中断是汇编语言级的,所以可能会发生两个线程同时访问同一个对象(包括全局变量、共享资源,API函数和 MFC对象等)的情况,这有可能导致程序错误。例如,如果一个线程在未完成对某一大尺寸全局变量的读操作时,另一个线程又对该变量进行了写操作,那么第一个线程读入的变量值可能是一种修改过程中的不稳定值。
属于不同进程的线程在同时访问同一内存区域或共享资源时,也会存在同样的问题。
因此,在多线程应用程序中,常常需要采取一些措施来同步线程的执行。
等待函数
Win32 API提供了一组能使线程阻塞其自身执行的等待函数。这些函数只有在作为其参数的一个或多个同步对象 (见下小节 )产生信号时才会返回。
在超过规定的等待时间后,不管有无信号,函数也都会返回。在等待函数未返回时,线程处于等待状态,此时线程只消耗很少的 CPU时间。
使用等待函数即可以保证线程的同步,又可以提高程序的运行效率。最常用的等待函数是 WaitForSingleObject,该函数的声明为:
DWORD WaitForSingleObject(HANDLE hHandle,DWORD dwMilliseconds);
参数 hHandle是同步对象的句柄。参数 dwMilliseconds是以毫秒为单位的超时间隔,如果该参数为 0,那么函数就测试同步对象的状态并立即返回,
如果该参数为 INFINITE,则超时间隔是无限的。
同步对象同步对象用来协调多线程的执行,它可以被多个线程共享。线程的等待函数用同步对象的句柄作为参数,同步对象应该是所有要使用的线程都能访问到的。
同步对象的状态要么是有信号的,要么是无信号的。同步对象主要有三种:事件,mutex和信号灯。
事件对象 (Event)是最简单的同步对象,它包括有信号和无信号两种状态。在线程访问某一资源之前,也许需要等待某一事件的发生,这时用事件对象最合适。
例如,只有在通信端口缓冲区收到数据后,监视线程才被激活。
mutex对象的状态在它不被任何线程拥有时是有信号的,而当它被拥有时则是无信号的。 mutex对象很适合用来协调多个线程对共享资源的互斥访问 (mutually
exclusive)。
信号灯对象维护一个从 0开始的计数,在计数值大于 0时对象是有信号的,而在计数值为 0时则是无信号的。信号灯对象可用来限制对共享资源进行访问的线程数量。线程用 CreateSemaphore函数来建立信号灯对象,在调用该函数时,可以指定对象的初始计数和最大计数。在建立信号灯时也可以为对象起个名字,别的进程中的线程可以用 OpenSemaphore函数打开指定名字的信号灯句柄。
关键节和互锁变量访问关键节 (Critical Seciton)与 mutex的功能类似,但它只能由同一进程中的线程使用。关键节可以防止共享资源被同时访问。
进程负责为关键节分配内存空间,关键节实际上是一个
CRITICAL_SECTION型的变量,它一次只能被一个线程拥有。在线程使用关键节之前,必须调用 InitializeCriticalSection函数将其初始化。
如果线程中有一段关键的代码不希望被别的线程中断,那么可以调用
EnterCriticalSection函数来申请关键节的所有权,在运行完关键代码后再用 LeaveCriticalSection函数来释放所有权。如果在调用
EnterCriticalSection时关键节对象已被另一个线程拥有,那么该函数将无限期等待所有权。
利用互锁变量可以建立简单有效的同步机制。使用函数
InterlockedIncrement和 InterlockedDecrement可以增加或减少多个线程共享的一个 32位变量的值,并且可以检查结果是否为 0。线程不必担心会被其它线程中断而导致错误。如果变量位于共享内存中,那么不同进程中的线程也可以使用这种机制。
串行通信与重叠 I/O
Win 32 系统为串行通信提供了全新的服务 。 传统的 OpenComm、
ReadComm,WriteComm,CloseComm 等函数已经过时,
WM_COMMNOTIFY消息也消失了 。 取而代之的是文件 I/O函数提供的打开和关闭通信资源句柄及读写操作的基本接口 。
新的文件 I/O函数 (CreateFile,ReadFile,WriteFile等 )支持重叠式输入输出,
这使得线程可以从费时的 I/O操作中解放出来,从而极大地提高了程序的运行效率 。
串行口的打开和关闭
Win 32系统把文件的概念进行了扩展。无论是文件、通信设备、命名管道、邮件槽、磁盘、还是控制台,都是用 API函数 CreateFile来打开或创建的。该函数的声明为:
HANDLE CreateFile(
LPCTSTR lpFileName,// 文件名
DWORD dwDesiredAccess,// 访问模式
DWORD dwShareMode,//共享模式
PSECURITY_ATTRIBUTES lpSecurityAttributes,//通常为 NULL
DWORD dwCreationDistribution,//创建方式
DWORD dwFlagsAndAttributes,//文件属性和标志
HANDLE hTemplateFile // 临时文件的句柄,通常为 NULL);
如果调用成功,那么该函数返回文件的句柄,如果调用失败,则函数返回 INVALID_HANDLE_VALUE。
串行口的初始化在打开通信设备句柄后,常常需要对串行口进行一些初始化工作 。 这需要通过一个 DCB结构来进行 。 DCB结构包含了诸如波特率,每个字符的数据位数,奇偶校验和停止位数等信息 。 在查询或配置置串行口的属性时,
都要用 DCB结构来作为缓冲区 。
调用 GetCommState函数可以获得串口的配置,该函数把当前配置填充到一个 DCB 结构中 。 一般在用 CreateFile 打开串行口后,可以调用
GetCommState函数来获取串行口的初始配置 。 要修改串行口的配置,应该先修改 DCB结构,然后再调用 SetCommState函数用指定的 DCB结构来设置串行口 。
在用 ReadFile和 WriteFile读写串行口时,需要考虑超时问题。如果在指定的时间内没有读出或写入指定数量的字符,那么 ReadFile或 WriteFile的操作就会结束。要查询当前的超时设置应调用 GetCommTimeouts函数,该函数会填充一个 COMMTIMEOUTS结构。调用 SetCommTimeouts可以用某一个 COMMTIMEOUTS结构的内容来设置超时。
重叠 I/O
在用 ReadFile和 WriteFile读写串行口时,既可以同步执行,也可以重叠
( 异步 ) 执行 。 在同步执行时,函数直到操作完成后才返回 。 这意味着在同步执行时线程会被阻塞,从而导致效率下降 。 在重叠执行时,即使操作还未完成,调用的函数也会立即返回 。 费时的 I/O操作在后台进行,
这样线程就可以干别的事情 。 例如,线程可以在不同的句柄上同时执行
I/O操作,甚至可以在同一句柄上同时进行读写操作 。,重叠,一词的含义就在于此 。
ReadFile函数只要在串行口输入缓冲区中读入指定数量的字符,就算完成操作 。 而 WriteFile函数不但要把指定数量的字符拷入到输出缓冲中,
而且要等这些字符从串行口送出去后才算完成操作 。
ReadFile和 WriteFile函数是否为执行重叠操作是由 CreateFile函数决定的。
如果在调用 CreateFile创建句柄时指定了 FILE_FLAG_OVERLAPPED标志,那么调用 ReadFile和 WriteFile对该句柄进行的读写操作就是重叠的,
如果未指定重叠标志,则读写操作是同步的。
通信事件在 Windows 95/NT中,WM_COMMNOTIFY消息已经取消,
在串行口产生一个通信事件时,程序并不会收到通知消息 。
线程需要调用 WaitCommEvent函数来监视发生在串行口中的各种事件,该函数的第二个参数返回一个事件屏蔽变量,用来指示事件的类型 。 线程可以用 SetCommMask建立事件屏蔽以指定要监视的事件,表 8.4列出了可以监视的事件 。 调用
GetCommMask可以查询串行口当前的事件屏蔽 。