多任务和多线程
多任务是一个操作系统可以同时执行多个程序的能力。基本上,操作系统使用一个硬件时钟为同时执行的每个程序配置「时间片段」。如果时间片段够小,并且机器也没有由于太多的程序而超出负荷时,那么在使用者看来,所有的这些程序似乎在同时执行着。
多任务并不是什么新的东西。在大型计算机上,多任务是必然的。这些大型主机通常有几十甚至几百个终端机和它连结,而每个终端机使用者都应该感觉到他或者她独占了整个计算机。另外,大型主机的操作系统通常允许使用者「提交工作到背景」,这些背景作业可以在使用者进行其它工作时,由机器执行完成。
个人计算机上的多任务花了更长的时间才普及化。但是现在PC多任务也被认为是很正常的了。我马上就会讨论到,MicrosoftWindows的16位版本支持有限度的多任务,Windows的32位版本支持真正的多任务,而且,还多了一种额外的优点,多线程。
多线程是在一个程序内部实作多任务的能力。程序可以把它自己分隔为各自独立的「线程」,这些线程似乎也同时在执行着。这一概念初看起来似乎没有什么用处,但是它可以让程序使用多执行绪在背景执行冗长作业,从而让使用者不必长时间地无法使用其计算机进行其它工作(有时这也许不是人们所希望的,不过这种时候去冲冲凉或者到冰箱去看看总是很不错的)!但是,即使在计算机繁忙的时候,使用者也应该能够使用它。
多任务的各种模式
在PC的早期,有人曾经提倡未来应该朝多任务的方向前进,但是大多数的人还是很迷惑:在一个单使用者的个人计算机上,多任务有什么用呢?好了,最后事实表示即使是不知道这一概念的使用者也都需要多任务的。
DOS下的多任务
在最初PC上的Intel8088微处理器并不是为多任务而设计的。部分原因(我在上一章中讨论过)是内存管理不够强。当启动和结束多个程序时,多任务的操作系统通常需要移动内存块以收集空闲内存。在8088上是不可能透明于应用系统来做到这一点的。
DOS本身对多任务没有太大的帮助,它的设计目的是尽可能小巧,并且与独立于应用程序之外,因此,除了加载程序以及对程序提供文件系统的存取功能,它几乎没有提供任何支持。
不过,有创意的程序写作者仍然在DOS的早期就找到了一种克服这些缺陷的方法,大多数是使用常驻(TSR:terminate-and-stay-resident)程序。有些TSR,比如背景打印队列程序等,透过拦截硬件时钟中断来执行真正的背景处理。其它的TSR,诸如SideKick等弹出式工具,可以执行某种型态的工作切换-暂停目前的应用程序,执行弹出式工具。DOS也逐渐有所增强以便提供对TSR的支持。
一些软件厂商试图在DOS之上架构出工作切换或者多任务的外壳程序(shell)(诸如Quarterdeck的DesqView),但是在这些环境中,仅有其中一个占据了大部分市场,当然,这就是Windows。
非优先权式的多任务
当Microsoft在1985年发表Windows1.0时,它是最成熟的解决方案,目的是突破DOS的局限。Windows在实际模式下执行。但是即使这样,它已可以在物理内存中移动内存块。这是多任务的前提,虽然移动的方法尚未完全透明于应用程序,但是几乎可以忍受了。
在图形窗口环境中,多任务比在一种命令列单使用者操作系统中显得更有意义。例如,在传统的命令列UNIX中,可以在命令列之外执行程序,让它们在背景执行。然而,程序的所有显示输出必须被重新转向到一个文件中,否则输出将和使用者正在做的事情混在一起。
窗口环境允许多个程序在相同屏幕上一起执行,前后切换非常容易,并且还可以快速地将数据从一个程序移动到另一个程序中。例如,将绘图程序中建立的图片嵌入由文书处理程序编辑的文本文件中。在Windows中,以多种方式支持数据转移,首先是使用剪贴簿,后来又使用动态数据交换(DDE),而现在则是透过对象连结和嵌入(OLE)。
不过,早期Windows的多任务实作还不是多使用者操作系统中传统的优先权式的分时多任务。这些操作系统使用系统时钟周期性地中断一个工作并开始另一个工作。Windows的这些16位版本支持一种被称为「非优先权式的多任务」,由于Windows消息驱动的架构而使这种型态的多任务成为可能。通常情况下,一个Windows程序将在内存中睡眠,直到它收到一个消息为止。这些消息通常是使用者的键盘或鼠标输入的直接或间接结果。当处理完消息之后,程序将控制权返回给Windows。
Windows的16位版本不会绝对地依据一个timertick将控制权从一个Windows程序切换到另一个,任何的工作切换都发生在当程序完成对消息的处理后将控制权返回给Windows时。这种非优先权式的多任务也被称为「合作式的多任务」,因为它要求来自应用程序方面的一些合作。一个Windows程序可以占用整个系统,如果它要花很长一段时间来处理消息的话。
虽然非优先权式的多任务是16位Windows的一般规则,但仍然出现了某些形式的优先权式多任务。Windows使用优先权式多任务来执行DOS程序,而且,为了实作多媒体,还允许动态链接库接收硬件时钟中断。
16位Windows包括几个功能特性来帮助程序写作者解决(或者,至少可以说是对付)非优先权式多任务中的局限,最显着的当然是时钟式鼠标光标。当然,这并非一种解决方案,而仅仅是让使用者知道一个程序正在忙于处理一件冗长作业,因而让使用者在一段时间内无法使用系统。另一种解决方案是Windows定时器,它允许程序周期性地接收消息并完成一些工作。定时器通常用于时钟应用和动画。
针对非优先权式多任务的另一种解决方案是PeekMessage函数呼叫,我们曾在第五章中的RANDRECT程序里看到过。一个程序通常使用GetMessage呼叫从它的消息队列中找寻下一个消息,不过,如果在消息队列中没有消息,那么GetMessage不会传回,一直到出现一个消息为止。而另一方面,PeekMessage将控制权传回程序,即使没有等待的消息。这样,一个程序可以执行一个冗长作业,并在程序代码中混入PeekMessage呼叫。只要没有这个程序或其它任何程序的消息要处理,那么这个冗长作业将继续执行。
PresentationManager和序列化的消息队列
Microsoft在一种半DOS/半Windows的环境下实作多任务的第一个尝试(和IBM合作)是OS/2和PresentationManager(缩写成PM)。虽然OS/2明确地支持优先权式多任务,但是这种多任务方式似乎并未在PresentationManager中得以落实。问题在于PM序列化来自键盘和鼠标的使用者输入消息。这意味着,在前一个使用者输入消息被完全处理以前,PM不会将一个键盘或者鼠标消息传送给程序。
尽管键盘和鼠标消息只是一个PM(或者Windows)程序可以接收的许多消息中的几个,大多数的其它消息都是键盘或者鼠标事件的结果。例如,菜单命令消息是使用者使用键盘或者鼠标进行菜单选择的结果。在处理菜单命令消息时,键盘或者鼠标消息并未完全被处理。
序列化消息队列的主要原因是允许使用者的预先「键入」键盘按键和预先「按入」鼠标按钮。如果一个键盘或者鼠标消息导致输入焦点从一个窗口切换到另一个窗口,那么接下来的键盘消息应该进入拥有新的输入焦点的窗口中去。因此,系统不知道将下一个使用者输入消息发送到何处,直到前一个消息被处理完为止。
目前的共识是不应该让一个应用系统有可能占用整个系统,而这需要非序列化的消息队列,32位版本的Windows支持这种消息队列。如果一个程序正在忙着处理一项冗长作业,那么您可以将输入焦点切换到另一个程序中。
多线程解决方案
我讨论OS/2的PresentationManager,只是因为它是第一个为早期的Windows程序写作者(比如我自己)介绍多线程的环境。有趣的是,PM实作多线程的局限为程序写作者提供了应该如何架构多线程程序的必要线索。即使这些限制在32位的Windows中已经大幅减少,但是从更有限的环境中学到的经验仍然是非常有效的。因此,让我们继续讨论下去。
在一个多线程环境中,程序可以将它们自己分隔为同时执行的片段(叫做执行绪)。对执行绪的支持是解决PM中存在的序列化消息队列的最好方法,并且在Windows中线程有更实际的意义。
就程序代码来说,一个线程简单地被表示为可能呼叫程序中其它函数的函数。程序从其主线程开始执行,这个主执行绪是在传统的C程序中叫做main的函数,而在Windows中是WinMain。一旦执行起来,程序可以通过在系统呼叫CreateThread中指定初始线程函数的名称来建立新的线程的执行。操作系统在执行绪之间优先权式地切换控件,和它在程序之间切换控制权的方法非常类似。
在OS/2的PresentationManager中,每个线程可以建立一个消息队列,也可以不建立。如果希望从线程建立窗口,那么一个PM线程必须建立消息队列。否则,如果只是进行许多的数据处理或者图形输出,那么线程不需要建立消息队列。因为无消息队列的程序不处理消息,所以它们将不会当住系统。唯一的限制是一个无消息队列线程无法向一个消息队列线程中的窗口发送消息,或者呼叫任何发送消息的函数(不过,它们可以将消息递送给消息队列线程)。
这样,PM程序写作者学会了如何将它们的程序分隔为一个消息队列线程(在其中建立所有的窗口并处理传送给窗口的消息)和一个或者多个无消息队列线程,在其中执行冗长的背景工作。PM程序写作者还了解到「1/10秒规则」,大体上,程序写作者被告知,一个消息队列线程处理任何消息都不应该超过1/10秒,任何花费更长时间的事情都应该在另一个线程中完成。如果所有的程序写作者都遵循这一规则,那么将没有PM程序会将系统当住超过1/10秒。
多线程架构
我已经说过PM的限制让程序写作者理解如何在图形环境中执行的程序里头使用多个执行绪提供了必要的线索。因此在这里我将为您的程序建议一种架构:您的主执行绪建立您程序所需要的所有窗口,并在其中包含所有的窗口消息处理程序,以便处理这些窗口的所有消息;所有其它执行绪只进行一些背景处理,除了和主执行绪通讯,它们不和使用者进行交流。
可以把这种架构想象成:主线程处理使用者输入(和其它消息),并建立程序中的其它线程,这些附加的线程完成与使用者无关的工作。
换句话说,您程序的主线程是一个老板,而您的其它线程是老板的职员。老板将大的工作丢给职员处理,而他自己保持和外界的联系。因为那些线程仅仅是职员,所以其它线程不会举行它们自己的记者招待会。它们会认真地完成自己的工作,将结果报告给老板,并等待他们的下一个任务。
一个程序中的线程是同一程序的不同部分,因此他们共享程序的资源,如内存和打开的文件。因为线程共享程序的内存,所以他们还共享静态变量。然而,每个线程都有他们自己的堆栈,因此动态变量对每个线程是唯一的。每个线程还有各自的处理器状态(和数学协处理器状态),这个状态在进行线程切换期间被储存和恢复。
线程间的「争吵」
正确地设计、写作和测试一个复杂的多线程应用程序显然是Windows程序写作者可能遇到的最困难的工作之一。因为优先权式多任务系统可以在任何时刻中断一个线程,并将控制权切换到另一个线程中,在两个线程之间可能有无法预料的随机交互作用的情况。
多线程程序中的一个常见的错误被称为「竞争状态(racecondition)」,这发生在程序写作者假设一个线程在另一个线程需要某资料之前已经完成了某些处理(如准备数据)的时候。为了帮助协调线程的活动,操作系统要求各种形式的同步。一种是同步信号(semaphore),它允许程序写作者在程序代码中的某一点阻止一个线程的执行,直到另一个执行绪发信号让它继续为止。类似于同步信号的是「临界区域(criticalsection)」,它是程序代码中不可中断的部分。
但是同步信号还可能产生称为「死锁(deadlock)」的常见线程错误,这发生在两个线程互相阻止了另一个的执行,而继续执行的唯一办法又是它们继续向前执行。
幸运的是,32位程序比16位程序更能抵抗线程所涉及的某些问题。例如,假定一个线程执行下面的简单叙述:
lCount++ ;
其中lCount是由其它线程使用的一个32位的long型态变量,C中的这个叙述被编译为两条机械码指令,第一条将变量的低16位加1,而第二条指令将任何可能的进位加到高16位上。假定操作系统在这两个机械码指令之间中断了线程。如果lCount在第一条机械码指令之前是0x0000FFFF,那么lCount在线程被中断时为0,而这正是另一个线程将看到的值。只有当线程继续执行时,lCount才会增加到正确的值0x00010000。
这是那些偶尔会导致操作问题的错误之一。在16位程序中,解决此问题正确的方法是将叙述包含在一个临界区域中,在这期间线程不会被中断。然而,在一个32位程序中,该叙述是正确的,因为它被编译为一条机械码指令。
Windows的好处
32位Windows版本(包括Windows NT和Windows98)有一个非序列化的消息队列。这种实作似乎非常好:如果一个程序正在花费一段长时间处理一个消息,那么鼠标位于该程序的窗口上时,鼠标光标将呈现为一个时钟,但是当将鼠标移到另一个程序的窗口上时,鼠标光标将变为正常的箭头形状。只需按一下就可以将另一个窗口提到前面来。
然而,使用者仍然不能使用正在处理大量工作的那个程序,因为那些工作会阻止程序接收其它消息,这不是我们所希望的。一个程序应该总是能随时处理消息的,所以这时就需要使用从属线程了。
在Windows NT和Windows98中,没有消息队列线程和无消息队列线程的区别,每个线程在建立时都会有它自己的消息队列,从而减少了PM程序中关于线程的一些不便规定(然而,在大多数情况下,您仍然想通过一条专门处理消息的线程中的消息程序处理输入,而将冗长作业交给那些不包含窗口的线程处理,这种结构几乎总是最容易理解的,我们将看到这一点)。
还有更好的事情:Windows NT和Windows98中有个函数允许线程杀死同一程序中的另一个线程。当您开始编写多线程程序代码时,您将会发现这种功能在有时是很方便的。OS/2的早期版本没有「杀死线程」的函数。
最后的好消息(至少对这里的话题是好消息)是Windows NT和Windows98实作了一些被称为「线程区域储存空间(TLS:thread localstorage)」的功能。为了了解这一点,回顾一下我在前面提到过的,静态变量(对一个函数来说,既是整体又是区域变量)在线程之间是被共享的,因为它们位于程序的数据储存空间中。动态变量(对一个函数来说总是区域变量)对每一个线程则是唯一的,因为它们占据堆栈上的空间,而每个线程都有它自己的堆栈。
有时让两个或多个线程使用相同的函数,而让这些线程使用唯一于线程的静态变量,那会带来很大便利。这就是线程区域储存空间,其中涉及一些Windows函数呼叫,但是Microsoft还为C编译器进行扩展,使线程区域储存空间的使用更透明于程序写作者。
新改良过的!支持多线程了!
既然已经介绍了线程的现状,让我们来展望一下线程的未来。有时,有人会出现一种使用操作系统所提供的每一种功能特性的冲动。最坏的情况是,当您的老板走到您的桌前并说:「我听说这种新功能非常炫,让我们在自己的程序中用一些这种新功能吧。」然后您将花费一个星期的时间,试图去了解您的应用程序如何从这种新功能获益。
应该注意的是,在并不需要多线程的应用系统中加入多线程是没有任何意义的。如果您的程序显示沙漏光标的时间太长,或者如果它使用PeekMessage呼叫来避免沙漏光标的出现,那么请重新规划您的程序架构,使用多线程可能会是一个好主意。其它情形,您是在为难您自己,并可能会在程序代码中产生新的错误。
在某些情况下,沙漏光标的出现可能是完全适当的。我在前面提到过「1/10秒规则」,而将一个大文件加载内存可能会花费多于1/10秒的时间,这是否意味着文件加载例程应该在分离的线程中实作呢?没有必要。当使用者命令一个程序打开文件时,他或者她通常想立即完成该操作。将文件加载例程放在分离的线程中只会增加额外的负担。即使您想向您的朋友夸耀您在编写多线程程序,也完全不值得这样做!
Windows 的多线程处理
建立新的线程的API函数是CreateThread,它的语法如下:
hThread = CreateThread (&security_attributes, dwStackSize, ThreadProc, pParam, dwFlags, &idThread) ;
第一个参数是指向SECURITY_ATTRIBUTES型态的结构的指针。在Windows98中忽略该参数。在WindowsNT中,它被设为NULL。第二个参数是用于新线程的初始堆栈大小,默认值为0。在任何情况下,Windows根据需要动态延长堆栈的大小。
CreateThread的第三个参数是指向线程函数的指标。函数名称没有限制,但是必须以下列形式声明:
DWORD WINAPI ThreadProc (PVOID pParam) ;
CreateThread的第四个参数为传递给ThreadProc的参数。这样主线程和从属线程就可以共享数据。
CreateThread的第五个参数通常为0,但当建立的线程不马上执行时为旗标CREATE_SUSPENDED。线程将暂停直到呼叫ResumeThread来恢复线程的执行为止。第六个参数是一个指标,指向接受执行绪ID值的变量。
大多数Windows程序写作者喜欢用在PROCESS.H表头文件中声明的C执行时期链接库函数_beginthread。它的语法如下:
hThread = _beginthread (ThreadProc, uiStackSize, pParam) ;
它更简单,对于大多数应用程序很完美,这个线程函数的语法为:
void __cdecl ThreadProc (void * pParam) ;
再论随机矩形
程序20-1 RNDRCTMT是第五章里的RANDRECT程序的多线程版本,您将回忆起RANDRECT使用的是PeekMessage循环来显示一系列的随机矩形。
程序20-1 RNDRCTMTRNDRCTMT.C /*--------------------------------------------------------------------------- RNDRCTMT.C -- Displays Random Rectangles (c) Charles Petzold, 1998 -------------------------------------------------------------------------*/ #include <windows.h> #include <process.h> LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; HWNDhwnd ; int cxClient, cyClient ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("RndRctMT") ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName= NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow ( szAppName, TEXT ("Random Rectangles"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } VOID Thread (PVOID pvoid) { HBRUSH hBrush ; HDC hdc ; int xLeft, xRight, yTop, yBottom, iRed, iGreen, iBlue ; while (TRUE) { if (cxClient != 0 || cyClient != 0) { xLeft = rand () % cxClient ; xRight= rand () % cxClient ; yTop = rand () % cyClient ; yBottom = rand () % cyClient ;iRed = rand () & 255 ; iGreen= rand () & 255 ; iBlue = rand () & 255 ; hdc = GetDC (hwnd) ; hBrush = CreateSolidBrush (RGB (iRed, iGreen, iBlue)) ; SelectObject (hdc, hBrush) ; Rectangle (hdc,min (xLeft, xRight), min (yTop, yBottom), max (xLeft, xRight), max (yTop, yBottom)) ; ReleaseDC (hwnd, hdc) ; DeleteObject (hBrush) ; } } } LRESULT CALLBACK WndProc ( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WM_CREATE: _beginthread (Thread, 0, NULL) ; return 0 ; case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
在建立多线程的Windows程序时,需要在「ProjectSettings」对话框中做一些修改。选择「C/C++」页面标签,然后在「Category」下拉式清单方块中选择「CodeGeneration」。在「Use Run-TimeLibrary」下拉式清单方块中,可以看到用于「Release」设定的「Single-Threaded」和用于Debug设定的「DebugSingle-Threaded」。将这些分别改为「Multithreaded」和「DebugMultithreaded」。这将把编译器旗标改为/MT,它是编译器在编译多线程的应用程序所需要的。具体地说,编译器将在.OBJ文件中插入LIBCMT.LIB文件名,而不是LIBC.LIB。连结程序使用这个名称与执行期链接库函数连结。
LIBC.LIB和LIBCMT.LIB文件包含C语言链接库函数,有些C语言链接库函数包含静态数据。例如,由于strtok函数可能被连续地多次呼叫,所以它在静态内存中储存了一个指标。在多线程程序中,每个线程必须在strtok函数中有它自己的静态指针。因此,这个函数的多线程版本稍微不同于单线程的strtok函数。
同时请注意,我在RNDRCTMT.C中包含了表头文件PROCESS.H,这个文件定义一个名为_beginthread的函数,它启动一个新的线程。只有定义了_MT标识符,才会声明这个函数,这是/MT旗标的另一个结果。
在RNDRCTMT.C的WinMain函数中,由CreateWindow传回的hwnd值被储存在一个整体变量中,因此cxClient和cyClient值也可以由窗口消息处理程序的WM_SIZE消息获得。
窗口消息处理程序以最容易的方法呼叫_beginthread-简单地以线程函数的地址(称为Thread)作为第一个参数,其它参数使用0,线程函数传回VOID并有一个参数,该参数是一个指向VOID的指标。在RNDRCTMT中的Thread函数不使用这个参数。
在呼叫了_beginthread函数之后,线程函数(以及该线程函数可能呼叫的其它任何函数)中的程序代码和程序中的其它程序代码同时执行。两个或者多个执行绪使用一个程序中的同一函数,在这种情况下,动态区域变量(储存在堆栈上)对每个执行绪是唯一的。对程序中的所有执行绪来说,所有的静态变量都是一样的。这就是窗口消息处理程序设定整体的cxClient和cyClient变量并由Thread函数使用的方式。
有时您需要唯一于各个线程的持续储存性数据。通常,这种数据是静态变量,但在Windows98中,您可以使用「线程区域储存空间」,我将在本章后面进行讨论。
程序设计竞赛的问题
1986年10月3日,Microsoft举行了为期一天,针对计算机杂志出版社的技术编辑和作者的简短的记者招待会,来讨论他们当时的一组语言产品,包括他们的第一个交谈式开发环境,QuickBASIC2.0。当时,Windows1.0出现还不到一年,但是没有人知道我们什么时候能得到与该环境类似的东西(这花了好几年)。这一事件与众不同的部分原因是由于Microsoft的公关人员所举办的「Stormthe Gates」程序设计竞赛。Bill Gates使用QuickBASIC2.0,而计算机出版社的人员可以使用他们选择的任何语言产品。
竞赛的问题是从公众提出的题目中挑选出来的(挑选那些需要写大约半小时程序来解决的问题),问题如下:
建立一个包含四个窗口的多任务仿真程序。第一个窗口必须显示一系列的递增数,第二个必须显示一系列的递增质数,而第三个必须显示Fibonacci数列(Fibonacci数列以数字0和1开始,后头每一个数都是其前两个数的和-即0、1、1、2、3、5、8等等)。这三个窗口应该在数字达到窗口底部时或者进行滚动,或者自行清除窗口内容。第四个窗口必须显示任意半径的圆,而程序必须在按下一个Escape键时终止。
当然,在1986年10月,在DOS下执行的这样一个程序最多只能是模拟多任务而已,而且没有一个竞赛者具有足够的勇气-并且其中大多数也没有足够的知识-来为Windows编写这个程序。再者,如果真要这么做,当然不会只花半小时了!
参加这次竞赛的大多数人编写了一个程序来将屏幕分为四个区域,程序中包含一个循环,依次更新每个窗口,然后检查是否按下了Escape键。如同DOS环境下的传统习惯,程序占用了百分之百的CPU处理时间。
如果在Windows 1.0中写程序,那么结果将是类似程序20-2MULTI1的结果。我说「类似」,是因为我编写的程序是32位的,但程序结构和相当多的程序代码-除了变量和函数参数定义以及Unicode支持-都是相同的。
程序20-2 MULTI1MULTI1.C /*-------------------------------------------------------------------------- MULTI1.C -- Multitasking Demo (c) Charles Petzold, 1998 ----------------------------------------------------------------------------*/ #include <windows.h> #include <math.h> LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int cyChar ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("Multi1") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground= (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName= szAppName ; if (!RegisterClass (&wndclass)) { MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow ( szAppName, TEXT ("Multitasking Demo"),WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } int CheckBottom (HWND hwnd, int cyClient, int iLine) { if (iLine * cyChar + cyChar > cyClient) { InvalidateRect (hwnd, NULL, TRUE) ; UpdateWindow (hwnd) ; iLine = 0 ; } return iLine ; } // ------------------------------------------------------------------------- // Window 1: Display increasing sequence of numbers // ------------------------------------------------------------------------- LRESULT APIENTRY WndProc1 (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static int iNum, iLine, cyClient ; HDC hdc ; TCHARszBuffer[16] ; switch (message) { case WM_SIZE: cyClient = HIWORD (lParam) ; return 0 ; case WM_TIMER: if (iNum < 0) iNum = 0 ; iLine = CheckBottom (hwnd, cyClient, iLine) ; hdc = GetDC (hwnd) ; TextOut (hdc, 0, iLine * cyChar, szBuffer, wsprintf (szBuffer, TEXT ("%d"), iNum++)) ; ReleaseDC (hwnd, hdc) ; iLine++ ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; } // -------------------------------------------------------------------------- // Window 2: Display increasing sequence of prime numbers // -------------------------------------------------------------------------- LRESULT APIENTRY WndProc2 (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static intiNum = 1, iLine, cyClient ; HDC hdc ; int i, iSqrt ; TCHARszBuffer[16] ; switch (message) { case WM_SIZE: cyClient = HIWORD (lParam) ; return 0 ; case WM_TIMER: do{ if (++iNum < 0)iNum = 0 ; iSqrt = (int) sqrt (iNum) ; for (i = 2 ; i <= iSqrt ; i++)if (iNum % i == 0)break ; } while (i <= iSqrt) ; iLine = CheckBottom (hwnd, cyClient, iLine) ; hdc = GetDC (hwnd) ; TextOut ( hdc, 0, iLine * cyChar, szBuffer,wsprintf (szBuffer, TEXT ("%d"), iNum)) ; ReleaseDC (hwnd, hdc) ; iLine++ ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; } // -------------------------------------------------------------------------- // Window 3: Display increasing sequence of Fibonacci numbers // -------------------------------------------------------------------------- LRESULT APIENTRY WndProc3 (HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam) { static intiNum = 0, iNext = 1, iLine, cyClient ; HDChdc ; int iTemp ; TCHARszBuffer[16] ; switch (message) { case WM_SIZE: cyClient = HIWORD (lParam) ; return 0 ; case WM_TIMER: if (iNum < 0) { iNum = 0 ; iNext = 1 ; } iLine = CheckBottom (hwnd, cyClient, iLine) ; hdc = GetDC (hwnd) ; TextOut ( hdc, 0, iLine * cyChar, szBuffer, wsprintf (szBuffer, "%d", iNum)) ; ReleaseDC (hwnd, hdc) ; iTemp = iNum ; iNum = iNext ; iNex += iTemp ; iLine++ ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; } // -------------------------------------------------------------------------- // Window 4: Display circles of random radii // --------------------------------------------------------------------------- LRESULT APIENTRY WndProc4 (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static intcxClient, cyClient ; HDC hdc ; int iDiameter ; switch (message) { case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_TIMER: InvalidateRect (hwnd, NULL, TRUE) ;UpdateWindow (hwnd) ; iDiameter = rand() % (max (1, min (cxClient, cyClient))) ; hdc = GetDC (hwnd) ; Ellipse (hdc, (cxClient - iDiameter) / 2, (cyClient - iDiameter) / 2, (cxClient + iDiameter) / 2, (cyClient + iDiameter) / 2) ; ReleaseDC (hwnd, hdc) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; } // -------------------------------------------------------------------------- // Main window to create child windows // -------------------------------------------------------------------------- LRESULT APIENTRY WndProc ( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static HWND hwndChild[4] ; static TCHAR * szChildClass[] = { TEXT ("Child1"), TEXT ("Child2"), TEXT ("Child3"), TEXT ("Child4") } ; static WNDPROCChildProc[] = { WndProc1, WndProc2, WndProc3, WndProc4 } ; HINSTANCE hInstance ; int i, cxClient, cyClient ; WNDCLASS wndclass ; switch (message) { case WM_CREATE: hInstance = (HINSTANCE) GetWindowLong (hwnd, GWL_HINSTANCE) ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = NULL ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground= (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; for (i = 0 ; i < 4 ; i++) { wndclass.lpfnWndProc = ChildProc[i] ; wndclass.lpszClassName = szChildClass[i] ; RegisterClass (&wndclass) ; hwndChild[i] = CreateWindow (szChildClass[i], NULL, WS_CHILDWINDOW | WS_BORDER | WS_VISIBLE, 0, 0, 0, 0, hwnd, (HMENU) i, hInstance, NULL) ; } cyChar = HIWORD (GetDialogBaseUnits ()) ; SetTimer (hwnd, 1, 10, NULL) ; return 0 ; case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; for (i = 0 ; i < 4 ; i++) MoveWindow (hwndChild[i], (i % 2) * cxClient / 2, (i > 1) * cyClient / 2,cxClient / 2, cyClient / 2, TRUE) ; return 0 ; case WM_TIMER: for (i = 0 ; i < 4 ; i++) SendMessage (hwndChild[i], WM_TIMER, wParam, lParam) ; return 0 ; case WM_CHAR: if (wParam == '\x1B') DestroyWindow (hwnd) ; return 0 ; case WM_DESTROY: KillTimer (hwnd, 1) ; PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
在这个程序里实际上没有什么我们没见过的东西。主窗口建立四个子窗口,每个子窗口占据显示区域的一个象限。主窗口还设定一个Windows定时器并发送WM_TIMER消息给四个子窗口中的每一个。
通常一个Windows程序应该保留足够的信息以便在WM_PAINT消息处理期间重建其窗口中的内容。MULTI1没有这么做,既然它绘制和清除窗口的速度如此之快,所以我认为那是不必要的。
WndProc2中的质数产生器的效率并不很高,但是有效。如果一个数除了1和它自身以外没有别的因子,那么这个数就是质数。当然,要检查一个数是否是质数并不要求使用小于被检查数的所有数来除这个数并检查余数,而只需使用所有小于被检查数的平方根的数。平方根计算是发表浮点数的原因,否则,该程序将是完全依据整数的程序。
MULTI1程序没有什么不好的地方。使用Windows定时器是在Windows的早期(和目前)版本中模拟多任务的一种好方法,然而,定时器的使用有时限制了程序的速度。如果程序可以在WM_TIMER消息处理中更新它的所有窗口而还有时间剩余下来的话,那就意味着它并没有充分利用我们的机器资源。
一种可能的解决方案是在单个WM_TIMER消息处理期间进行两次或者更多次的更新,但是到底多少次呢?这不得不依赖于机器的速度,而有很大的变动性。您当然不会想编写一个只能适用于25MHz的386或50MHz的486或100-GHz的PentiumVII上的程序吧。
多线程解决方案
让我们来看一看关于这个程序设计问题的一种多线程解决方案。如程序20-3MULTI2所示。
程序20-3 MULTI2MULTI2.C /*--------------------------------------------------------------------------- MULTI2.C -- Multitasking Demo (c) Charles Petzold, 1998 ----------------------------------------------------------------------------*/ #include <windows.h> #include <math.h> #include <process.h> typedef struct { HWND hwnd ; int cxClient ; int cyClient ; int cyChar ; BOOL bKill ; } PARAMS, *PPARAMS ; LRESULT APIENTRY WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("Multi2") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName= NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow ( szAppName, TEXT ("Multitasking Demo"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } int CheckBottom (HWND hwnd, int cyClient, int cyChar, int iLine) { if (iLine * cyChar + cyChar > cyClient) { InvalidateRect (hwnd, NULL, TRUE) ; UpdateWindow (hwnd) ;iLine = 0 ; } return iLine ; } // -------------------------------------------------------------------------- // Window 1: Display increasing sequence of numbers // -------------------------------------------------------------------------- void Thread1 (PVOID pvoid) { HDC hdc ; int iNum = 0, iLine = 0 ; PPARAMS pparams ; TCHAR szBuffer[16] ; pparams = (PPARAMS) pvoid ; while (!pparams->bKill) { if (iNum < 0) iNum = 0 ; iLine = CheckBottom ( pparams->hwnd,pparams->cyClient, pparams->cyChar,iLine) ; hdc = GetDC (pparams->hwnd) ; TextOut ( hdc, 0, iLine * pparams->cyChar, szBuffer,wsprintf (szBuffer, TEXT ("%d"), iNum++)) ; ReleaseDC (pparams->hwnd, hdc) ; iLine++ ; } _endthread () ; } LRESULT APIENTRY WndProc1 (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static PARAMS params ; switch (message) { case WM_CREATE: params.hwnd = hwnd ; params.cyChar = HIWORD (GetDialogBaseUnits ()) ; _beginthread (Thread1, 0, 秏s) ; return 0 ; case WM_SIZE: params.cyClient = HIWORD (lParam) ; return 0 ; case WM_DESTROY: params.bKill = TRUE ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; } // -------------------------------------------------------------------------- // Window 2: Display increasing sequence of prime numbers // -------------------------------------------------------------------------- void Thread2 (PVOID pvoid) { HDC hdc ; int iNum = 1, iLine = 0, i, iSqrt ; PPARAMS pparams ; TCHAR szBuffer[16] ; pparams = (PPARAMS) pvoid ; while (!pparams->bKill) { do { if (++iNum < 0)iNum = 0 ; iSqrt = (int) sqrt (iNum) ; for (i = 2 ; i <= iSqrt ; i++) if (iNum % i == 0)break ; } while (i <= iSqrt) ; iLine = CheckBottom ( pparams->hwnd, pparams->cyClient, pparams->cyChar,iLine) ; hdc = GetDC (pparams->hwnd) ; TextOut ( hdc, 0, iLine * pparams->cyChar, szBuffer, wsprintf (szBuffer, TEXT ("%d"), iNum)) ; ReleaseDC (pparams->hwnd, hdc) ; iLine++ ; } _endthread () ; } LRESULT APIENTRY WndProc2 (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static PARAMS params ; switch (message) { case WM_CREATE: params.hwnd = hwnd ; params.cyChar = HIWORD (GetDialogBaseUnits ()) ; _beginthread (Thread2, 0, 秏s) ; return 0 ; case WM_SIZE: params.cyClient = HIWORD (lParam) ; return 0 ; case WM_DESTROY: params.bKill = TRUE ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; } // Window 3: Display increasing sequence of Fibonacci numbers // ---------------------------------------------------------- void Thread3 (PVOID pvoid) { HDC hdc ; int iNum = 0, iNext = 1, iLine = 0, iTemp ; PPARAMS pparams ; TCHAR szBuffer[16] ; pparams = (PPARAMS) pvoid ; while (!pparams->bKill) { if (iNum < 0) { iNum = 0 ; iNext = 1 ; } iLine = CheckBottom ( pparams->hwnd,pparams->cyClient, pparams->cyChar, iLine) ; hdc = GetDC (pparams->hwnd) ; TextOut (hdc, 0, iLine * pparams->cyChar, szBuffer, wsprintf (szBuffer, TEXT ("%d"), iNum)) ;ReleaseDC (pparams->hwnd, hdc) ; iTemp = iNum ; iNum = iNext ; iNext += iTemp ; iLine++ ; } _endthread () ; } LRESULT APIENTRY WndProc3 (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static PARAMS params ; switch (message) { case WM_CREATE: params.hwnd = hwnd ; params.cyChar = HIWORD (GetDialogBaseUnits ()) ; _beginthread (Thread3, 0, 秏s) ; return 0 ; case WM_SIZE: params.cyClient = HIWORD (lParam) ; return 0 ; case WM_DESTROY: params.bKill = TRUE ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; } // ------------------------------------------------------------------------- // Window 4: Display circles of random radii // ------------------------------------------------------------------------- void Thread4 (PVOID pvoid) { HDC hdc ; int iDiameter ; PPARAMS pparams ; pparams = (PPARAMS) pvoid ; while (!pparams->bKill) { InvalidateRect (pparams->hwnd, NULL, TRUE) ; UpdateWindow (pparams->hwnd) ; iDiameter = rand() % (max (1, min (pparams->cxClient, pparams->cyClient))) ; hdc = GetDC (pparams->hwnd) ; Ellipse (hdc, (pparams->cxClient - iDiameter) / 2, (pparams->cyClient - iDiameter) / 2, (pparams->cxClient + iDiameter) / 2, (pparams->cyClient + iDiameter) / 2) ; ReleaseDC (pparams->hwnd, hdc) ; } _endthread () ; } LRESULT APIENTRY WndProc4 (HWND hwnd, UINT message,WPARAM wParam,LPARAM lParam) { static PARAMS params ; switch (message) { case WM_CREATE: params.hwnd = hwnd ; params.cyChar = HIWORD (GetDialogBaseUnits ()) ; _beginthread (Thread4, 0, 秏s) ; return 0 ; case WM_SIZE: params.cxClient = LOWORD (lParam) ; params.cyClient = HIWORD (lParam) ; return 0 ; case WM_DESTROY: params.bKill = TRUE ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; } // -------------------------------------------------------------------------- // Main window to create child windows // -------------------------------------------------------------------------- LRESULT APIENTRY WndProc ( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static HWND hwndChild[4] ; static TCHAR * szChildClass[] = { TEXT ("Child1"), TEXT ("Child2"), TEXT ("Child3"), TEXT ("Child4") } ; static WNDPROC ChildProc[] = { WndProc1, WndProc2, WndProc3, WndProc4 } ; HINSTANCE hInstance ; int i, cxClient, cyClient ; WNDCLASS wndclass ; switch (message) { case WM_CREATE: hInstance = (HINSTANCE) GetWindowLong (hwnd, GWL_HINSTANCE) ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = NULL ; wndclass.hCursor= LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; for (i = 0 ; i < 4 ; i++) { wndclass.lpfnWndProc = ChildProc[i] ; wndclass.lpszClassName= szChildClass[i] ; RegisterClass (&wndclass) ; hwndChild[i] = CreateWindow (szChildClass[i], NULL, WS_CHILDWINDOW | WS_BORDER | WS_VISIBLE, 0, 0, 0, 0, hwnd, (HMENU) i, hInstance, NULL) ; } return 0 ; case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; for (i = 0 ; i < 4 ; i++) MoveWindow (hwndChild[i], (i % 2) * cxClient / 2,(i > 1) * cyClient / 2, cxClient / 2, cyClient / 2, TRUE) ; return 0 ; case WM_CHAR: if (wParam == '\x1B') DestroyWindow (hwnd) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
MULTI2.C的WinMain和WndProc函数非常类似于MULTI1.C中的同名函数。WndProc为四个窗口注册了四种窗口类别,建立了这些窗口,并在WM_SIZE消息处理期间缩放这些窗口。WndProc的唯一不同是它不再设定Windows定时器,也不再处理WM_TIMER消息。
MULTI2中较大的改变是每个子窗口消息处理程序透过在WM_CREATE消息处理期间呼叫_beginthread函数来建立另一个线程。总括来说,MULTI2程序有五个同时执行的执行绪,主执行绪包含主窗口消息处理程序和四个子窗口消息处理程序,其余的四个执行绪使用名为Thread1、Thread2等的函数,这四个线程负责绘制四个窗口。
我在RNDRCTMT程序中给出的多线程程序代码没有使用_beginthread的第三个参数,这个参数允许一个建立另一个线程的线程在32位变量中将信息传递给其它线程。通常,这个变量是一个指针,而且是指向一个结构的指针,这允许原来的线程和新线程共享信息,而不必借助于整体变量。您可以看到,在MULTI2中没有整体变量。
对MULTI2程序,我在程序开头定义了一个名为PARAMS的结构和一个名为PPARAMS的指向结构的指针,这个结构有五个字段-窗口句柄、窗口的宽度和高度、字符的高度和名为bKill的布尔变数。最后的结构字段允许建立线程告知被建立线程何时终止。
让我们来看一看WndProc1,这是显示增加数序列的子窗口消息处理程序。窗口消息处理程序变得非常简单,唯一的区域变量是一个PARAMS结构。在WM_CREATE消息处理期间,它设定这个结构的hwnd和cyChar字段,呼叫_beginthread来建立一个使用Thread1函数的新线程,并传递给新线程一个指向该结构的指针。在WM_SIZE消息处理期间,WndProc1设定结构的cyClient字段,而在WM_DESTROY消息处理期间,它将bKill字段设定为TRUE。Thread1函数通过对_endthread的呼叫而告结束。这并不是绝对必要的,因为线程将在退出线程函数之后被清除。不过,要退出一个深陷入复杂的处理程序的线程时,_endthread是很有用的。
Thread1函数完成在窗口上的实际绘图,并且和程序的其它四个线程同时执行。函数接收指向PARAMS结构的一个指针,并进入一个while循环,不断检查bKill是TRUE还是FALSE。如果是FALSE,那么函数必须进行MULTI1.C中的WM_TIMER消息处理期间所作的同样处理-格式化数字、取得设备内容句柄并使用TextOut显示数字。
当您在Windows98中执行MULTI2时,将会看到,窗口更新要比在MULTI1中快得多,这表示程序在更加有效地利用处理器的资源。在MULTI1和MULTI2之间还有另一种区别:通常,当您移动或者缩放一个窗口时,内定窗口消息处理程序进入一种模态循环,而窗口的所有输出都将停止。在MULTI2中,输出将继续。
有问题吗?
似乎MULTI2程序并没有达到它应该有的稳固性。我为什么会这样认为呢?让我们来看一看MULTI2.C中的一些多线程「缺陷」,以WndProc1和Thread1为例。
WndProc1在MULTI2的主线程中执行,而Thread1与它同时执行,Windows98在这两个线程之间进行切换是不可预测的。假定Thread1正在执行,并且刚好执行了检查PARAMS结构的bKill字段是否为TRUE的程序代码。发现不为TRUE,但是这之后Windows98将控制权切换到主线程,这时使用者终止了程序,WndProc1收到一个WM_DESTROY消息并将bKill参数设为TRUE。哦,这参数设定得太晚了!操作系统突然切换到Thread1中,而该函数会试图取得一个不存在的窗口的设备内容句柄。
事实证明,这不是一个问题。Windows98够稳固,以致另一条线程呼叫的图形处理函数只是失败而已,而不会引起任何问题。
正确的多线程程序写作技术涉及线程同步的使用(尤其是临界区域的使用),我将马上加以详细地讨论。大体上,临界区域通过对EnterCriticalSection和LeaveCriticalSection的呼叫而加以界定。如果一个线程进入一个临界区域,那么另一个线程将无法再进入这个临界区域。后一个线程被阻档在对EnterCriticalSection的呼叫上,直到第一个线程呼叫LeaveCriticalSection时为止。
在MULTI2中的另一个可能存在的问题是,当另外一个线程显示其输出时,主线程可能会收到一个WM_ERASEBKGND或WM_PAINT消息。这里,使用临界区域有助于避免当两个程序试图在同一个窗口上绘图时可能导致的任何问题。但是,经验显示,Windows98很恰当地序列化了对图形绘制函数的存取。亦即,当另一个线程正在绘图的时候,一个线程不能在同一个窗口上绘图。
Windows98文件提醒说,有一种未进行图形函数序列化的情形,这就是GDI对象(如画笔、画刷、字体、位图、区域和调色盘等)的使用。有可能发生一个线程清除了一个对象,而另一个线程仍然在使用它的情况。解决这个问题的方法要求使用临界区域,或者最好不要在线程之间共享GDI对象。
Sleep的好处
我曾经提到,我认为对一个多线程程序来说,最好的架构是主线程建立程序中的所有窗口,以及所有的窗口消息处理程序,并处理所有的窗口消息。其它线程完成背景工作或者冗长作业。
不过,假设您想在另一个线程中做动画。通常,Windows中的动画是使用WM_TIMER消息来实作的。如果这个线程没有建立窗口,那么它也不会收到这些消息。如果没有定时器,动画又可能会执行得太快。
解决方案是Sleep函数。实际上,线程呼叫Sleep函数来自动暂停执行,该函数唯一一个参数是以毫秒计的时间。Sleep函数呼叫在指定的时间过去以前不会传回控制权。在这段时间内,线程被暂停,并且不会被配置给时间片段(尽管该线程显然仍然要求在tick时给予一小段的处理时间,因为系统必须确定线程是否应该重新开始执行)。给Sleep一个值为0的参数将导致线程交回它尚未使用完的时间片段。
当一个线程呼叫Sleep时,只是该线程被暂停指定的时间。系统仍然执行其它的执行绪,这些执行绪和暂停的执行绪可以是在同一个程序中,也可以是在另一个程序中。我在第十四章中的SCRAMBLE程序中使用了Sleep函数,以放慢画面清除的操作。
通常,您不应该在您的主线程中使用Sleep函数,因为这会减慢对消息的处理速度,但是因为SCRAMBLE没有建立任何窗口,因此在那里使用Sleep应该没有问题。
线程同步
大约每年一次,在我公寓窗外的交通繁忙地段的红绿灯会停止工作。结果是造成交通的混乱,虽然轿车一般能避免撞上别的轿车,但是这些车经常挤在一起。
我用术语称两条路相交的十字路口为「临界区域」。一辆向南的车和一辆向西的车不可能同时通过一个十字路口而不撞着对方。依赖于交通流量,可以采用不同的方法来解决这个问题。对于视野清楚车辆稀少的路口,可以相信司机有处理的能力。车辆增多可能会要求一个停车号志,而更加繁忙的交通则将要求有红绿灯,红绿灯有助于协调路口的交通(当然,这些灯号必须正常工作)。
临界区域
在单工操作系统中,传统的计算机程序不需要红绿灯来帮助协调它们之间的行为。它们在执行时似乎独占了整条路,而且也确实是这样,没有什么会干扰它们的工作。
即使在多任务操作系统中,大多数的程序也似乎各自独立地在执行,但是可能会发生一些问题。例如,两个程序可能会需要同时从同一个文件中读或者对同一文件进行写。在这种情况下,操作系统提供了一种共享文件和记录上锁的技术来帮助解决这个问题。
然而,在支持多线程的操作系统中,情况会变得混乱而且存在潜在的危险。两个或多个线程共享某些数据的情况并不罕见。例如,一个线程可以更新一个或者多个变量,而另一个线程可以使用这些变量。有时这会引发一个问题,有时又不会(记住操作系统将控制权从一个线程切换到另一个线程的操作,只能在机器码指令之间发生。如果只是一个整数被线程共享,那么对这个变量的改变通常发生在单个指令中,因此潜在的问题被最小化了)。
然而,假设线程共享几个变量或者数据结构。通常,这么多个变量或者结构的字段在它们之间必须是一致的。操作系统可以在更新这些变量的程序中间中断一个线程,那么使用这些变量的线程得到的将是不一致的数据。
结果是冲突发生了,并且通常不难想象这样的错误将对程序造成怎样的破坏。我们所需要的是类似于红绿灯的程序写作技术,以帮助我们对线程交通进行协调和同步,这就是临界区域。大体上,一个临界区域就是一块不可中断的程序代码。
有四个函数用于临界区域。要使用这些函数,您必须定义一个临界区域对象,这是一个型态为CRITICAL_SECTION的整体变量。例如:
CRITICAL_SECTION cs ;
这个CRITICAL_SECTION数据型态是一个结构,但是其中的字段只能由Windows内部使用。这个临界区域对象必须先被程序中的某个线程初始化,通过呼叫:
InitializeCriticalSection (&cs) ;
这样就建立了一个名为cs的临界区域对象。该函数的在线辅助说明包含下面的警告:「临界区域对象不能被移动或者复制,程序也不能修改该对象,但必须在逻辑上把它视为不透明的。」这句话,可以被解释为:「不要干扰它,甚至不要看它。」
当临界区域对象被初始化之后,线程可以通过下面的呼叫进入临界区域:
EnterCriticalSection (&cs) ;
在这时,线程被认为「拥有」临界区域对象。两个线程不可以同时拥有同一个临界区域对象,因此,如果一个线程进入了临界区域,那么下一个使用同一临界区域对象呼叫EnterCriticalSection的线程将在函数呼叫中被暂停。只有当第一个线程通过下面的呼叫离开临界区域时,函数才会传回控制权:
LeaveCriticalSection (&cs) ;
这时,在EnterCriticalSection呼叫中被停住的那个线程将拥有临界区域,其函数呼叫也将传回,允许线程继续执行。
当临界区域不再被程序所需要时,可以通过呼叫
DeleteCriticalSection (&cs) ;
将其删除,该函数释放所有被配置来维护此临界区域对象的系统资源。
这种临界区域技术涉及「互斥」(此术语在我们继续讨论线程同步时将再次出现)。在任何时刻,只有一个线程能拥有一个临界区域。因此,一个线程可以进入一个临界区域,设定一个结构的字段,然后退出临界区域。另一个使用该结构的线程在存取结构中的字段之前也要先进入该临界区域,然后再退出临界区域。
注意,您可以定义多个临界区域对象,比如cs1和cs2。例如,如果一个程序有四个线程,而前两个线程共享一些数据,那么它们可以使用一个临界区域对象,而另外两个线程共享一些其它的数据,那么它们可以使用另一个临界区域对象。
您在主线程中使用临界区域时应该小心。如果从属线程在它自己的临界区域中花费了一段很长的时间,那么它可能会将主线程的执行阻碍很长一段时间。从属执行绪可能只是使用临界区域复制该结构的字段到自己的区域变量中。
临界区域的一个限制是它们只能用于在同一程序内的线程之间的协调。但是在某些情况下,您需要协调两个不同程序对同一资源的共享(如共享内存等)。在此其况下不能使用临界区域,但是可以使用一种被称为「互斥对象(mutexobject)」的技术。「mutex」是个合成字,代表「mutualexclusion(互斥)」,它在这里精确地表达了我们的目的。我们想防止一个程序的线程在更新数据或者使用共享内存与其它资源时被中断。
事件信号
多线程通常是用于那些必须执行长时间处理的程序。我们可以将一个「大作业」定义为一个可能会违反1/10秒规则的程序。显然大作业包括文书处理程序中的拼写检查、数据库程序中的文件排序或者索引、电子表格的重新计算、打印,甚至包括复杂的绘图。当然,迄今为止我们知道,遵循1/10秒规则的最好方法是将大作业放到另一个线程去执行。这些额外的执行绪不会建立窗口,因此它们不受1/10秒规则的限制。
通常希望这些额外的线程在完成其任务时能够通知主线程,或者主线程能够停止其它线程正在进行的作业。这就是我们下面将要讨论的。
BIGJOB1程序
作为一个想象的大作业,我将使用一系列浮点运算,有时这种运算被称为「暴力的」性能测试指针。这种计算以一种间接的方式递增一个整数的值:它求一个数的平方,再对结果取平方根(得到原来的整数),然后使用log和exp函数(同样得到原来的整数),接着使用atan和tan函数(还是得到原来的整数),最后对结果加1。
BIGJOB1程序如程序20-4所示。
程序20-4 BIGJOB1BIGJOB1.C /*--------------------------------------------------------------------------- BIGJOB1.C -- Multithreading Demo (c) Charles Petzold, 1998 ----------------------------------------------------------------------------*/ #include <windows.h> #include <math.h> #include <process.h> #define REP 1000000 #define STATUS_READY 0 #define STATUS_WORKING1 #define STATUS_DONE 2 #define WM_CALC_DONE (WM_USER + 0) #define WM_CALC_ABORTED(WM_USER + 1) typedef struct { HWND hwnd ; BOOL bContinue ; } PARAMS, *PPARAMS ; LRESULT APIENTRY WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("BigJob1") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground= (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName= szAppName ; if (!RegisterClass (&wndclass)) { MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, TEXT ("Multithreading Demo"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } void Thread (PVOID pvoid) { doubleA = 1.0 ; INT i ; LONG lTime ; volatile PPARAMS pparams ; pparams = (PPARAMS) pvoid ; lTime = GetCurrentTime () ; for (i = 0 ; i < REP && pparams->bContinue ; i++) A = tan (atan (exp (log (sqrt (A * A))))) + 1.0 ; if (i == REP) { lTime = GetCurrentTime () - lTime ; SendMessage (pparams->hwnd, WM_CALC_DONE, 0, lTime) ; } else SendMessage (pparams->hwnd, WM_CALC_ABORTED, 0, 0) ; _endthread () ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static INT iStatus ; static LONG lTime ; static PARAMSparams ; staticTCHAR *szMessage[] = { TEXT ("Ready (left mouse button begins)"), TEXT ("Working (right mouse button ends)"), TEXT ("%d repetitions in %ld msec") } ; HDC hdc ; PAINTSTRUCT ps ; RECT rect ; TCHARszBuffer[64] ; switch (message) { case WM_LBUTTONDOWN: if (iStatus == STATUS_WORKING) { MessageBeep (0) ; return 0 ; } iStatus = STATUS_WORKING ; params.hwnd = hwnd ; params.bContinue = TRUE ; _beginthread (Thread, 0, 秏s) ; InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case WM_RBUTTONDOWN: params.bContinue = FALSE ; return 0 ; case WM_CALC_DONE: lTime = lParam ; iStatus = STATUS_DONE ; InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case WM_CALC_ABORTED: iStatus = STATUS_READY ; InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; GetClientRect (hwnd, &rect) ; wsprintf (szBuffer, szMessage[iStatus], REP, lTime) ; DrawText (hdc, szBuffer, -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
这是一个相当简单的程序,但是我认为您将看到它如何展示在多线程程序中完成大作业的通用方法。为了使用BIGJOB1程序,在窗口的显示区域中按下鼠标左键,从而开始暴力的性能测试计算的1,000,000次重复,这在一台300MHz的PentiumII机器上将花费2秒。当完成计算时,花费的时间将显示在窗口上。当正在进行计算时,您可以通过在显示区域中按下鼠标右键来终止它。
让我们来看一看这是如何实作的:
窗口消息处理程序拥有了一个被叫做iStatus的静态变量(该变量可以被设定为在程序开始处定义的三个常数之一,常数以STATUS为前缀),该变量表示程序是否准备好进行一次计算,是否正在进行一次计算,或者是否完成了计算。程序在WM_PAINT消息处理期间使用iStatus变量在显示区域的中央显示一个适当的字符串。
窗口消息处理程序还拥有一个静态结构(型态为PARAMS,也定义在程序的顶部),该结构是在窗口消息处理程序和其它线程之间的共享数据。结构只有两个字段-hwnd(程序窗口的句柄)和bContinue,这是一个布尔变量,用于指示线程是否继续计算或者停止。
当您在显示区域中按下鼠标左键时,窗口消息处理程序将iStatus变量设为STATUS_WORKING,并设定PARAMS结构中的两个字段。结构的hwnd字段被设定为窗口句柄,当然,bContinue被设定为TRUE。
然后窗口过程调用_beginthread函数。线程函数Thread以呼叫GetCurrentTime开始,GetCurrentTime取得以毫秒计的Windows启动以来已经执行了的时间。然后它进入一个for循环,重复1,000,000次的暴力测试计算。还要注意,如果bContinue被设为了FALSE,那么线程将退出循环。
在for循环之后,线程函数检查它是否确实完成了1,000,000次计算。如果是,那么它再次呼叫GetCurrentTime获得所经过的时间,然后使用SendMessage向窗口消息处理程序发送一个由程序定义的WM_USER_DONE消息,并以经过的时间作为lParam参数。如果计算是在未完成之前被终止的(即,如果在循环期间PARAMS结构的bContinue字段变为FALSE),那么线程将发送给窗口消息处理程序一个WM_USER_ABORTED消息。然后,线程通过呼叫_endthread正常地结束。
在窗口消息处理程序中,当您在显示区域中按下鼠标右键时,PARAMS结构的bContinue字段被设为FALSE。这是如何在完成计算之前结束计算的方法。
注意Thread中的pparams变量定义为volatile,这种型态限定字向编译器指出变量可能会在实际的程序叙述外被修改(例如被另一个线程)。否则,最佳化的编译器会假设pparams->bContinue不能被for循环内的程序代码修改,没有必要在每层循环中检查变量。volatile关键词防止这样的最佳化进行。
窗口消息处理程序处理WM_USER_DONE消息时,首先储存经过的时间。对WM_USER_DONE和WM_USER_ABORTED消息的处理都是透过对InvalidateRect的呼叫产生WM_PAINT消息并在显示区域显示一个新的字符串。
提供一个方法(如结构中的bContinue字段)允许线程正常终止,通常是一个好主意。KillThread函数只有在正常终止线程比较困难时才应该使用,原因是线程可以配置资源,如内存等。如果当线程终止时没有释放所配置的内存,那么内存将仍然是被配置了的。线程不是程序:所配置的资源在一个程序的所有线程之间是共享的,因此当线程终止时,资源不会被自动释放。好的程序结构要求一个线程释放由它配置的所有资源。
您还应该知道当第二个线程仍在执行时,可以建立第三个执行绪。如果Windows在SendMessage呼叫和_endthread呼叫之间,将控制权从第二个线程切换到第一个线程,那么窗口消息处理程序就可能响应鼠标按键而建立一个新的线程,从而出现了上述的情况。这不是什么问题,但是如果这对您自己的应用来说是一个问题的话,那么您可能会考虑使用临界区域来避免线程之间的冲突。
事件对象
BIGJOB1在每次需要执行暴力测试计算时,就建立一个执行绪。执行绪在完成计算之后自动终止。
另一种可用的方法是在程序的整个生命周期内保持线程的执行,但是只在必要时才启动它。这是一个应用事件对象的理想情况。
事件对象可以是「有信号的」(也称为「被设立的」)或「没信号的」(也称为「被重置的」)。您可以通过下面呼叫来建立事件对象:
hEvent = CreateEvent (&sa, fManual, fInitial, pszName) ;
第一个参数(指向一个SECURITY_ATTRIBUTES结构的指针)和最后一个参数(一个事件对象的名字)只有在事件对象被多个程序共享时才有意义。在同一程序中,这些参数通常被设定为NULL。如果您希望事件对象被初始化为有信号的,那么将fInitial参数设定为TRUE。而如果希望事件对象被初始化为无信号的,则将fInitial参数设定为FALSE。稍后,我将简短地描述fManual参数。
要设立一个现存的事件对象,呼叫
SetEvent (hEvent) ;
要重置一个事件对象,呼叫
ResetEvent (hEvent) ;
一个程序通常呼叫:
WaitForSingleObject (hEvent, dwTimeOut) ;
并且将第二个参数设定为INFINITE。如果事件对象目前是被设立的,那么函数将立即传回,否则,函数将暂停线程直到事件对象被设立。如果您将第二个参数设定为一个以毫秒计的超时时间值,这样函数也可能在事件对象被设立之前传回。
如果最初的CreateEvent呼叫的fManual参数被设定为FALSE,那么事件对象将在WaitForSingleObject函数传回时自动重置。这种功能特性通常使得事件对象没有必要使用ResetEvent函数。
现在,我们可以来看一看程序20-5所示的BIGJOB2.C程序。
程序20-5 BIGJOB2BIGJOB2.C /*---------------------------------------------------------------------------- BIGJOB2.C -- Multithreading Demo (c) Charles Petzold, 1998 -----------------------------------------------------------------------------*/ #include <windows.h> #include <math.h> #include <process.h> #define REP 1000000 #define STATUS_READY 0 #define STATUS_WORKING1 #define STATUS_DONE 2 #define WM_CALC_DONE (WM_USER + 0) #define WM_CALC_ABORTED (WM_USER + 1) typedef struct { HWND hwnd ; HANDLE hEvent ; BOOL bContinue ; } PARAMS, *PPARAMS ; LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("BigJob2") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon= LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground= (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName= szAppName ; if (!RegisterClass (&wndclass)) { MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, TEXT ("Multithreading Demo"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } void Thread (PVOID pvoid) { doubleA = 1.0 ; INT i ; LONG lTime ; volatile PPARAMS pparams ; pparams = (PPARAMS) pvoid ; while (TRUE) { WaitForSingleObject (pparams->hEvent, INFINITE) ; lTime = GetCurrentTime () ; for (i = 0 ; i < REP && pparams->bContinue ; i++) A = tan (atan (exp (log (sqrt (A * A))))) + 1.0 ; if (i == REP) { lTime = GetCurrentTime () - lTime ; PostMessage (pparams->hwnd, WM_CALC_DONE, 0, lTime) ; } else PostMessage (pparams->hwnd, WM_CALC_ABORTED, 0, 0) ; } } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static HANDLEhEvent ; static INT iStatus ; static LONG lTime ; static PARAMS params ; staticTCHAR *szMessage[] = { TEXT ("Ready (left mouse button begins)"), TEXT ("Working (right mouse button ends)"), TEXT ("%d repetitions in %ld msec") } ; HDC hdc ; PAINTSTRUCT ps ; RECT rect ; TCHARszBuffer[64] ; switch (message) { case WM_CREATE: hEvent = CreateEvent (NULL, FALSE, FALSE, NULL) ; params.hwnd = hwnd ; params.hEvent = hEvent ; params.bContinue = FALSE ; _beginthread (Thread, 0, 秏s) ; return 0 ; case WM_LBUTTONDOWN: if (iStatus == STATUS_WORKING) { MessageBeep (0) ; return 0 ; } iStatus = STATUS_WORKING ; params.bContinue = TRUE ; SetEvent (hEvent) ;InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case WM_RBUTTONDOWN:params.bContinue = FALSE ; return 0 ; case WM_CALC_DONE: lTime = lParam ; iStatus = STATUS_DONE ; InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case WM_CALC_ABORTED: iStatus = STATUS_READY ; InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; GetClientRect (hwnd, &rect) ; wsprintf (szBuffer, szMessage[iStatus], REP, lTime) ; DrawText (hdc, szBuffer, -1, &rect,DT_SINGLELINE | DT_CENTER | DT_VCENTER) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
处理WM_CREATE消息时,窗口消息处理程序首先建立一个初始化为没信号的自动重置事件对象,然后建立线程。
Thread函数进入一个无限的while循环,在循环开始时首先呼叫WaitForSingleObject(注意PARAMS结构包括一个包含事件对象句柄的字段)。因为事件被初始化为重置的,所以线程的执行被阻挡在函数呼叫中。按下鼠标左键将导致窗口过程调用SetEvent,这将释放由WaitForSingleObject呼叫产生的第二个线程,并开始暴力测试计算。当计算完之后,线程再次呼叫WaitForSingleObject,但是由于第一次呼叫已经使事件对象重置,因此,线程将被暂停,直到再次按下鼠标。
在其它方面,程序几乎和BIGJOB1完全一样。
线程区域储存空间 (TLS)
多线程程序中的整体变量(以及任何被配置的内存)被程序中的所有线程共享。在一个函数中的局部静态变量也被使用函数的所有线程共享。一个函数中的局部动态变量是唯一于各个线程的,因为它们被储存在堆栈上,而每个线程有它自己的堆栈。
对各个线程唯一的持续性储存空间有存在的必要。例如,我在本章前面提到过的C中的strtok函数要求这种型态的储存空间。不幸的是,C语言不支持这类储存空间。但是Windows中提供了四个函数,它们实作了一种技术来做到这一点,并且Microsoft对C的扩充语法也支持它,这就叫做线程区域储存空间。
下面是API工作的方法:
首先,定义一个包含需要唯一于线程的所有数据的结构,例如:
typedef struct { int a ; int b ; } DATA, * PDATA ;
主线程呼叫TlsAlloc获得一个索引值:
dwTlsIndex = TlsAlloc () ;
这个值可以储存在一个整体变量中或者通过参数结构传递给线程函数。
线程函数首先为该数据结构配置内存,并使用上面所获得的索引值呼叫TlsSetValue:
TlsSetValue (dwTlsIndex, GlobalAlloc (GPTR, sizeof (DATA)) ;
该函数将一个指标和某个线程及某个线程索引相关联。现在,任何需要使用这个指标的函数(包括最初的线程函数本身)都可以包含如下所示的程序代码:
PDATA pdata ; ... pdata = (PDATA) TlsGetValue (dwTlsIndex) ;
现在函数可以设定或者使用pdata->a和pdata->b了。在线程函数终止以前,它释放配置的内存:
GlobalFree (TlsGetValue (dwTlsIndex)) ;
当使用该数据的所有线程都终止之时,主线程将释放索引:
TlsFree (dwTlsIndex) ;
这个程序刚开始可能令人有些迷惑,因此如果能看一看如何实作线程区域储存空间可能会有帮助(我不知道Windows实际上是如何实作的,但下面的方案是可能的)。首先,TlsAlloc可能只是配置一块内存(长度为0)并传回一个索引值,即指向这块内存的一个指针。每次使用该索引呼叫TlsSetValue时,通过重新配置将内存块增大8个字节。在这8个字节中储存的是呼叫函数的线程ID(通过GetCurrentThreadId来获得)以及传递给TlsSetValue函数的指标。TlsSetValue简单地使用线程ID来搜寻操作系统管理的线程区域储存空间地址表,然后传回指标。TlsFree将释放内存块。所以您看,这可能是一件容易得可以由您自己来实作的事情。不过,既然已经有工具为您做好了这些工作,那也不错。
Microsoft对C的扩充功能使这件工作更加容易。只要在要对每个线程都保留不同内容的变量前加上__declspec(thread)就好了。对于任何函数的外部静态变量,则为:
__declspec (thread) int iGlobal = 1 ;
对于函数内部的静态变量,则为:
__declspec (thread) static int iLocal = 2 ;