关键代码段准确的描述
现在你已经从理论上对关键代码段有了一定的了解。已经知道为什么它们非常有用,以及它们是如何实现“以原子操作方式”对共享资源进行访问的。下面让我们更加深入地看一看关键代码段是如何运行的。首先介绍一下C R I T I C A L _ S E C T I O N数据结构。如果想查看一下Platform SDK文档中关于该结构的说明,也许你会感到无从下手。那么问题究竟何在呢?
并不是C R I T I C A L _ S E C T I O N结构没有完整的文档,而是M i c r o s o f t认为没有必要了解该结构的全部情况,这是对的。对于我们来说,这个结构是透明的,该结构有文档可查,但是该结构中的成员变量没有文档。当然,由于这只是个数据结构,可以在Wi n d o w s头文件中查找这些信息,可以看到这些数据成员( C R I T I C A L _ S E C T I O N在Wi n N T. h中定义为RT L _ C R I T I C A L _S E C T I O N;RT L _ C R I T I C A L _ S E C T I O N结构在Wi n B a s e . h中作了定义)。但是决不应该编写引用这些成员的代码。
若要使用C R I T I C A L _ S E C T I O N结构,可以调用一个Wi n d o w s函数,给它传递该结构的地址。该函数知道如何对该结构的成员进行操作,并保证该结构的状态始终一致。因此下面让我们将注意力转到这些函数上去。
通常情况下,C R I T I C A L _ S E C T I O N结构可以作为全局变量来分配,这样,进程中的所有线程就能够很容易地按照变量名来引用该结构。但是, C R I T I C A L _ S E C T I O N结构也可以作为局部变量来分配,或者从堆栈动态地进行分配。它只有两个要求,第一个要求是,需要访问该资源的所有线程都必须知道负责保护资源的C R I T I C A L _ S E C T I O N结构的地址,你可以使用你喜欢的任何机制来获得这些线程的这个地址;第二个要求是, C R I T I C A L _ S E C T I O N结构中的成员应该在任何线程试图访问被保护的资源之前初始化。该结构通过调用下面的函数来进行初始化:
VOID InitializeCriticalSection(PCRITICAL_SECTION pcs);
该函数用于对(p c s指向的)C R I T I C A L _ S E C T I O N结构的各个成员进行初始化。由于该函数只是设置了某些成员变量。因此它的运行不会失败,并且它的原型采用了V O I D的返回值。该函数必须在任何线程调用E n t e r C r i t i c a l S e c t i o n函数之前被调用。Platform SDK的文档清楚地说明,如果一个线程试图进入一个未初始化的C RT I C A L _ S E C T I O N,那么结果将是很难预计的。
当知道进程的线程不再试图访问共享资源时,应该通过调用下面的函数来清除该C R I T I C A L _ S E C T I O N结构:
VOID DeleteCriticalSection(PCRITICAL_SECTION pcs);
D e l e t e C r i t i c a l S e c t i o n函数用于对该结构中的成员变量进行删除。当然,如果有任何线程仍然使用关键代码段,那么不应该删除该代码段。同样, Platform SDK文档清楚地说明如果删除了关键代码段,其结果就无法知道。当编写要使用共享资源的代码时,必须在该代码的前面放置对下面的函数的调用:
VOID EnterCriticalSection(PCRITICAL_SECTION pcs);
E n t e r C r i t i c a l S e c t i o n函数负责查看该结构中的成员变量。这些变量用于指明当前是哪个变量正在访问该资源。E n t e r C r i t i c a l S e c t i o n负责进行下列测试:
• 如果没有线程访问该资源,E n t e r C r i t i c a l S e c t i o n便更新成员变量,以指明调用线程已被赋予访问权并立即返回,使该线程能够继续运行(访问该资源)。
• 如果成员变量指明,调用线程已经被赋予对资源的访问权,那么E n t e r C r i t i c a l S e c t i o n便更新这些变量,以指明调用线程多少次被赋予访问权并立即返回,使该线程能够继续运行。这种情况很少出现,并且只有当线程在一行中两次调用E n t e r C r i t i c a l S e c t i o n而不影响对L e a v e C r i t i c a l S e c t i o n的调用时,才会出现这种情况。
• 如果成员变量指明,一个线程(除了调用线程之外)已被赋予对资源的访问权,那么E n e r C r i t i c a l S e c t i o n将调用线程置于等待状态。这种情况是极好的,因为等待的线程不会浪费任何C P U 时间。系统能够记住该线程想要访问该资源并且自动更新C R I T I C A L _ S E C T I O N的成员变量,一旦目前访问该资源的线程调用L e a v e C r i t i c a l S e c t i o n函数,该线程就处于可调度状态。
从内部来讲, E n t e r C r i t i c a l S e c t i o n函数并不十分复杂。它只是执行一些简单的测试。为什么这个函数是如此有用呢?因为它能够以原子操作方式来执行所有的测试。如果在多处理器计算机上有两个线程在完全相同的时间同时调用E n t e r C r i t i c a l S e c t i o n函数,该函数仍然能够正确地起作用,一个线程被赋予对资源的访问权,而另一个线程则进入等待状态。
如果E n t e r C r i t i c a l S e c t i o n将一个线程置于等待状态,那么该线程在很长时间内就不能再次被调度。实际上,在编写得不好的应用程序中,该线程永远不会再次被赋予C P U时间。如果出现这种情况,该线程就称为渴求C P U时间的线程。
Windows 2000 在实际操作中,等待关键代码段的线程绝对不会渴求C P U时间。对E n t e r C r i t i c a l S e c t i o m的调用最终将会超时,导致产生一个异常条件。这时可以将一个调试程序附加给应用程序,以确定究竟出了什么问题。超时的时间量是由下面的注册表子关键字中包含的C r i t i c a l S e c t i o n Ti m e o u t数据值来决定的。
HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager
这个值以秒为单位,默认为2 592 000s,即大约3 0天。不要将这个值设置得太小(比如小于3 s),否则就会对系统中正常等待关键代码段超过3 s的线程和其他应用程序产生不利的影响。
可以使用下面这个函数来代替E n t e r C r i t i c a l S e c t i o n:
BOOL TryEnterCriticalSection(PCRITICAL_SECTION pcs);
Tr y E n t e r C r i t i c a l S e c t i o n函数决不允许调用线程进入等待状态。相反,它的返回值能够指明调用线程是否能够获得对资源的访问权。因此,如果Tr y E n t e r C r i t i c a l S e c t i o n发现该资源已经被另一个线程访问,它就返回FA L S E。在其他所有情况下,它均返回T R U E。
运用这个函数,线程能够迅速查看它是否可以访问某个共享资源,如果不能访问,那么它可以继续执行某些其他操作,而不必进行等待。如果Tr y E n t e r C r i t i c a l S e c t i o n函数确实返回了T R U E,那么C R I T I C A L _ S E C T I O N的成员变量已经更新,以便反映出该线程正在访问该资源。因此,对返回T R U E的Tr y E n t e r C r i t i c a l S e c t i o n函数的每次调用都必须与对L e a v e C r i t i c a l S e c t i o n函数的调用相匹配。
Windows 98没有可以使用的Tr y E n t e r C r i t i c a l S e c t i o n函数的实现代码。调用该函数总是返回FA L S E。
在接触共享资源的代码结尾处,必须调用下面这个函数:
VOID LeaveCriticalSection(PCRITICAL_SECTION pcs);
L e a v e C r i t i c a l S e c t i o n要查看该结构中的成员变量。该函数每次计数时要递减1,以指明调用线程多少次被赋予对共享资源的访问权。如果该计数大于0,那么L e a v e C r i t i c a l S e c t i o n不做其他任何操作,只是返回而已。
如果该计数变为0,它就要查看在调用E n t e r C r i t i c a l S e c t i o n中是否有别的线程正在等待。如果至少有一个线程正在等待,它就更新成员变量,并使等待线程中的一个线程(“公正地”选定)再次处于可调度状态。如果没有线程正在等待, L e a v e C r i t i c a l S e c t i o n函数就更新成员变量,以指明没有线程正在访问该资源。
与E n t e r C r i t i c a l S e c t i o n函数一样,L e a v e C r i t i c a l S e c t i o n函数也能以原子操作方式执行所有这些测试和更新。不过,L e a v e C r i t i c a l S e c t i o n从来不使线程进入等待状态,它总是立即返回。