• Windows核心编程 第九章 线程与内核对象的同步(下)


    9.4 等待定时器内核对象

        等待定时器是在某个时间或按规定的间隔时间发出自己的信号通知的内核对象。它们通常用来在某个时间执行某个操作。

        若要创建等待定时器,只需要调用C r e a t e Wa i t a b l e Ti m e r函数:

    HANDLE CreateWaitableTimer(

      PSECURITY_ATTRIBUTES psa,

      BOOL fManualReset,

      PCTSTR pszName

    );

    p s ap s z N a m e这两个参数在第3章中做过介绍。当然,进程可以获得它自己的与进程相关的现有等待定时器的句柄,方法是调用O p e n Wa i t a b l e Ti m e r函数:

    HANDLE OpenWaitableTimer(

        DWORD dwDesiredAccess,

        BOOL bInheritHandle,

        PCTSTR pszName

    );

        与事件的情况一样,f M a n u a l R e s e t参数用于指明人工重置的定时器或自动重置的定时器。当发出人工重置的定时器信号通知时,等待该定时器的所有线程均变为可调度线程。当发出自动重置的定时器信号通知时,只有一个等待的线程变为可调度线程。

        等待定时器对象总是在未通知状态中创建。必须调用 S e t Wa i t a b l e Ti m e r函数来告诉定时器你想在何时让它成为已通知状态:

     

        这个函数带有若干个参数,使用时很容易搞混。显然, h Ti m e r参数用于指明你要设置的定时器。p D u e Ti m el P e r i o d两个参数是一道使用的。P D u e Ti m e r参数用于指明定时器何时应该第一次报时,而l P e r i o d参数则用于指明此后定时器应该间隔多长时间报时一次。下面的代码用于将定时器的第一次报时的时间设置在2 0 0 211日的下午1点钟,然后每隔6小时报时一次:

     

        代码首先对S Y S T E M T I M E结构进行初始化,该结构用于指明定时器何时第一次报时(发出信号通知) 。我将该时间设置为本地时间,即计算机所在时区的正确时间。 S e t Wa i t a b l e Ti m e r的第二个参数的原型是个常量LARGE_INTEGER *,因此它不能直接接受S Y S T E M T I M E结构。但是,F I L E T I M E结构和L A R G E _ I N T E G E R结构拥有相同的二进制格式,都包含两个 3 2位的值。因此,我们可以将S Y S T E M T I M E结构转换成F I L E T I M E结构。再一个问题是,S e t Wa i t a b l e Ti m e r希望传递给它的时间始终都采用世界协调时( U T C)的时间。调用L o c a l F i l e Ti m e To F i l e Ti m e函数,就可以很容易地进行时间的转换。

        由于F I L E T I M EL A R G E _ I N T E G E R结构具有相同的二进制格式,因此可以像下面这样将F I L E T I M E结构的地址直接传递给S e t Wa i t a b l e Ti m e r

     

        实际上,这是我最初的做法。但是这是个大错误。虽然 F I L E T I M EL A R G E _ I N T E G E R结构采用相同的二进制格式,但是这两个结构的调整要求不同。所有 F I L E T I M E结构的地址必须从一个3 2位的边界开始,而所有 L A R G E _ I N T E G E R结构的地址则必须从 6 4位的边界开始。调用S e t Wa i t a b l e Ti m e r函数和给它传递一个 F I L E T I M E结构时是否能够正确地运行,取决于F I L E T I M E结构是否恰好位于6 4位的边界上。但是,编译器能够确保L A R G E _ I N T E G E R结构总是从6 4位的边界开始,因此要进行的正确操作(也就是所有时间都能保证起作用的操作)是将F I L E T I M E的成员拷贝到L A R G E _ I N T E G E R的成员中,然后将L A R G E _ I N T E G E R的地址传递给S e t Wa i t a b l e Ti m e r

        注意 x 8 6处理器能够悄悄地处理未对齐的数据引用。因此当应用程序在 x86 CPU上运行时,将F I L E T I M E的地址传递给S e t Wa i t a b l r Ti m e r总是可行的。但是,其他处理器,如A l p h a处理器,则无法像x 8 6处理器那样悄悄地处理未对齐的数据引用。实际上,大多数其他处理器都会产生一个 E X C E P T I O N _ D ATAT Y P E _ M I S A L I G N M E N T异常,它会导致进程终止运行。当你将 x 8 6计算机上运行的代码移植到其他处理器时,产生问题的最大原因是出现了对齐错误。如果现在注意对齐方面的问题,就能够在以后省去几个月的代码移植工作。关于对齐问题的详细说明,参见第 1 3章。

        现在,若要使定时器在2 0 0 211日下午1点之后每隔6 h进行一次报时,我们应该将注意力转向l P e r i o d参数。该参数用于指明定时器在初次报时后每隔多长时间(以毫秒为单位)进行一次报时。如果是每隔6 h进行一次报时,那么我传递 21 600 0006 h×每小时6 0 m i n×每分钟6 0 s×每秒1 0 0 0 m s) 。另外,如果给它传递了以前的一个绝对时间,比如1 9 7 511日下午1点,那么S e t Wa i t a b l e Ti m e r的运行就会失败。

        如果不设置定时器应该第一次报时的绝对时间,也可以让定时器在一个相对于调用S e t Wa i t a b l e Ti m e r的时间进行报时。只需要在p D u e Ti m e参数中传递一个负值。传递的值必须是以1 0 0 n s为间隔。由于我们通常并不以 1 0 0 n s的间隔来思考问题,因此我们要说明一下 1 0 0 n s的具体概念:1 s = 1 0 0 0 m s = 1 0 0 0 0 0 0 µ s = 1 0 0 0 0 0 0 0 0 0 n s

    下面的代码用于将定时器设置为在调用S e t Wa i t a b l e Ti m e r函数后5 s第一次报时:

     

        通常情况下,你可能想要一个一次报时的定时器,它只是发出一次报时信号,此后再也不发出报时信号。若要做到这一点,只需要为 l P e r i o d参数传递0即可。然后可以调用C l o s e H a n d l e函数,关闭定时器,或者再次调用 S e t Wa i t a b l e Ti m e r函数,重新设置时间,为它规定一个需要遵循的新条件。

        S e t Wa i t a b l e Ti m e r的最后一个参数是f R e s u m e,它可以用于支持暂停和恢复的计算机。通常可以为该参数传递FA L S E,就像我在上面这个代码段中设置的那样。但是,如果你编写了一个会议安排类型的应用程序,在这个应用程序在中,你想设置一个为用户提醒会议时间安排的定时器,那么应该传递T R U E。当定时器报时的时候,它将使计算机摆脱暂停方式(如果它处于暂停状态的话) ,并唤醒等待定时器报时的线程。然后该应用程序运行一个波形文件,并显示一个消息框,告诉用户即将举行的会议。如果为 f R e s u m e参数传递FA L S E,定时器对象就变为已通知状态,但是它唤醒的线程必须等到计算机恢复运行(通常由用户将它唤醒)之后才能得到C P U时间。

    除了上面介绍的定时器函数外,最后还有一个C a n c e l Wa i t a b l e Ti m e r函数:

        BOOL CancelWaitableTimer(HANDLE hTimer);

    这个简单的函数用于取出定时器的句柄并将它撤消,这样,除非接着调用 S e t Wa i t a b l e Ti m e r函数以便重新设置定时器,否则定时器决不会进行报时。如果想要改变定时器的报时条件,不必在调用S e t Wa i t a b l e Ti m e r函数之前调用C a n c e l Wa i t a b l e Ti m e r函数。每次调用S e t Wa i t a b l e Ti m e r函数,都会在设置新的报时条件之前撤消定时器原来的报时条件。

    9.4.1 让等待定时器给A P C项排队

        到现在为止,你已经学会了如何创建定时器和如何设置定时器。你还知道如何通过将定时器的句柄传递给 Wa i t F o r S i n g l e O b j e c tWa i t F o r M u l t i p l e O b j e c t s函数,以便等待定时器报时。M i c r o s o f t还允许定时器给在定时器得到通知信号时调用 S e t Wa i t a b l e Ti m e r函数的线程的异步过程调用(A P C)进行排队。

        一般来说,当调用 S e t Wa i t a b l e Ti m e r函数时,你将同时为 p f n C o m p l e t i o n R o u t i n ep v A rg C o m p l e t i o n R o u t i n e参数传递N U L L。当S e t Wa i t a b l e Ti m e函数看到这些参数的N U L L时,它就知道,当规定的时间到来时,就向定时器发出通知信号。但是,如果到了规定的时间,你愿意让定时器给一个A P C排队,那么你必须传递定时器A P C例程的地址,而这个例程是你必须实现的。该函数应该类似下面的形式:


        我已经将该函数命名为Ti m e r A P C R o u t i n e,不过可以根据需要给它赋予任何一个名字。该函数可以在定时器报时的时候由调用S e t Wa i t a b l e Ti m e r函数的同一个线程来调用,但是只有在调用线程处于待命状态下才能调用。换句话说,该线程必须正在 S l e e p E x , Wa i t F o r S n g l e O b j e c t E xWa i t F o r M u l t i p l e O b j e c t s E xM s g Wa i t F o r M u l t i p l e O b j e c t s E xS i n g l e O b j e c t - A n d Wa i t等函数的调用中等待。如果该线程不在这些函数中的某个函数中等待,系统将不给定时器 A P C例程排队。这可以防止线程的A P C队列中塞满定时器A P C通知,这会浪费系统中的大量内存。

        当定时器报时的时候,如果你的线程处于待命的等待状态中,系统就使你的线程调用回调例程。回调例程的第一个参数的值与你传递给S e t Wa i t a b l e Ti m e r函数的p v A rg To C o m p l e t i o n R o u t i n e参数的值是相同的。你可以将某些上下文信息(通常是你定义的某个结构的指针)传递给Ti m e r A P C R o u t i n e。剩余的两个参数d w Ti m e r L o w Va l u ed w Ti m e r H i g h Va l u e用于指明定时器何时报时。下面的代码使用了该信息,并将它显示给用户:


        只有当所有的A P C项都已经处理之后,待命的函数才会返回。因此,必须确保定时器再次变为已通知状态之前,Ti m e r A P C R o u t i n e函数完成它的运行,这样,A P C项的排队速度就不会比它被处理的速度快。

    下面的代码显示了使用定时器和A P C项的正确方法:


        最后要说明的是,线程不应该等待定时器的句柄,也不应该以待命的方式等待定时器。请看下面的代码:

     

        不应该编写上面的代码,因为调用Wa i t F o r S i n g l e O b j e c t E x函数实际上是两次等待该定时器,一次是以待命方式等待,一次是等待内核对象句柄。当定时器变为已通知状态时,等待就成功了,线程被唤醒,这将使该线程摆脱待命状态,而 A P C例程则没有被调用。前面讲过,通常没有理由使用带有等待定时器的A P C例程,因为你始终都可以等待定时器变为已通知状态,然后做你想要做的事情。

    9.4.2 定时器的松散特性

        定时器常常用于通信协议中。例如,如果客户机向服务器发出一个请求,而服务器没有在规定的时间内作出响应,那么客户机就会认为无法使用服务器。目前,客户机通常要同时与许多服务器进行通信。如果你为每个请求创建一个定时器内核对象,那么系统的运行性能就会受到影响。可以设想,对于大多数应用程序来说,可以创建单个定时器对象,并根据需要修改定时器报时的时间。

        定时器报时时间的管理方法和定时器时间的重新设定是非常麻烦的,只有很少的应用程序采用这种方法。但是在新的线程共享函数(第 11 章中介绍)中有一个新函数,称为C r e a t e Ti m e r Q u e u e Ti m e r,它能够为你处理所有的操作。如果你发现自己创建和管理了若干个定时器对象,那么应该观察一下这个函数,以减少应用程序的开销。

    9.5 信标内核对象

        信标内核对象用于对资源进行计数。它们与所有内核对象一样,包含一个使用数量,但是它们也包含另外两个带符号的3 2位值,一个是最大资源数量,一个是当前资源数量。最大资源数量用于标识信标能够控制的资源的最大数量,而当前资源数量则用于标识当前可以使用的资源的数量。

        为了正确地说明这个问题,让我们来看一看应用程序是如何使用信标的。比如说,我正在开发一个服务器进程,在这个进程中,我已经分配了一个能够用来存放客户机请求的缓冲区。我对缓冲区的大小进行了硬编码,这样它每次最多能够存放 5个客户机请求。如果5个请求尚未处理完毕时,一个新客户机试图与服务器进行联系,那么这个新客户机的请求就会被拒绝,并出现一个错误,指明服务器现在很忙,客户机应该过些时候重新进行联系。当我的服务器进程初始化时,它创建一个线程池,里面包含 5个线程,每个线程都准备在客户机请求到来时对它进行处理。

        开始时,没有客户机提出任何请求,因此我的服务器不允许线程池中的任何线程成为可调度线程。但是,如果3个客户机请求同时到来,那么线程池中应该有 3个线程处于可调度状态。使用信标,就能够很好地处理对资源的监控和对线程的调度,最大资源数量设置为 5,因为这是我进行硬编码的缓冲区的大小。当前资源数量最初设置为 0,因为没有客户机提出任何请求。当客户机的请求被接受时,当前资源数量就递增,当客户机的请求被提交给服务器的线程池时,当前资源数量就递减。

        信标的使用规则如下:

        • 如果当前资源的数量大于0,则发出信标信号。

        • 如果当前资源数量是0,则不发出信标信号。

        • 系统决不允许当前资源的数量为负值。

        • 当前资源数量决不能大于最大资源数量。

        当使用信标时,不要将信标对象的使用数量与它的当前资源数量混为一谈。

        下面的函数用于创建信标内核对象:

    HANDLE CreateSemaphore(

    PSECURITY_ATTRIBUTE psa,

    LONG lInitialCount,

    LONG lMaximumCount,

    PCTSTR pszName

    );

        p s ap s z N a m e两个参数在第3章中作过介绍。当然,通过调用 O p e n S e m a p h o r e函数,另一个进程可以获得它自己的进程与现有信标相关的句柄:

        l M a x i m u m C o u n t参数用于告诉系统,应用程序处理的最大资源数量是多少。由于这是个带符号的3 2位值,因 此最多可以拥有2 147 483 647个资源。l I n i t i a l C o u n t参数用于指明开始时(当前)这些资源中有多少可供使用。当我的服务器进程初始化时,没有任何客户机请求,因此我调用下面这个C r e a t e S e m a p h o r e函数:

    HANDLE hsem = CreateSemaphore(NULL ,0 ,5 ,NULL);

        该函数创建了一个信标,其最大资源数量为 5,但是开始时可以使用的资源为0(由于偶然的原因,该内核对象的使用数量是1,因为我刚刚创建了这个内核对象,请不要与计数器搞混) 。由于当前资源数量被初始化为0,因此不发出信标信号。等待信标的所有线程均进入等待状态。

        通过调用等待函数,传递负责保护资源的信标的句柄,线程就能够获得对该资源的访问权。从内部来说,该等待函数要检查信标的当前资源数量,如果它的值大于0(信标已经发出信号) ,那么计数器递减1,调用线程保持可调度状态。信标的出色之处在于它们能够以原子操作方式来执行测试和设置操作,这就是说,当向信标申请一个资源时,操作系统就要检查是否有这个资源可供使用,同时将可用资源的数量递减,而不让另一个线程加以干扰。只有当资源数量递减后,系统才允许另一个线程申请对资源的访问权。

        如果该等待函数确定信标的当前资源数量是 0(信标没有发出通知信号) ,那么系统就调用函数进入等待状态。当另一个线程将对信标的当前资源数量进行递增时,系统会记住该等待线程(或多个线程) ,并允许它变为可调度状态(相应地递减它的当前资源数量) 。

        通过调用R e l e a s e S e m a p h o r e函数,线程就能够对信标的当前资源数量进行递增:

    BOOL ReleaseSemaphore(

        HANDLE hsem,

        LONG lReleaseCount,

        PLONG plPreviousCount

    );

        该函数只是将 l R e l e a s e C o u n t中的值添加给信标的当前资源数量。通常情况下,为l R e l e a s e C o u n t参数传递1,但是,不一定非要传递这个值。我常常传递 2或更大的值。该函数也能够在它的* p l P r e v i o u s C o u n t中返回当前资源数量的原始值。实际上几乎没有应用程序关心这个值,因此可以传递N U L L,将它忽略。

        有时,有必要知道信标的当前资源数量而不修改这个数量,但是没有一个函数可以用来查询信标的当前资源数量的值。起先我认为调用 R e l e a s e S e m a p h o r e并为l R e l e a s e C o u n t参数传递0,也许会在* p l P r e v i o u s C o u n t中返回资源的实际数量。但是这样做是不行的,R e l e a s e S e m a p h o r e0填入这个长变量。接着,我试图传递一个非常大的数字,作为第二个参数,希望它不会影响当前资源数量,因为它将取代最大值。同样,R e l e a s e S e m a p h o r e0填入* p l P r e v i o u s。可惜,如果不对它进行修改,就没有办法得到信标的当前资源数量。

    9.6 互斥对象内核对象

        互斥对象(m u t e x)内核对象能够确保线程拥有对单个资源的互斥访问权。实际上互斥对象是因此而得名的。互斥对象包含一个使用数量,一个线程 I D和一个递归计数器。互斥对象的行为特性与关键代码段相同,但是互斥对象属于内核对象,而关键代码段则属于用户方式对象。这意味着互斥对象的运行速度比关键代码段要慢。但是这也意味着不同进程中的多个线程能够访问单个互斥对象,并且这意味着线程在等待访问资源时可以设定一个超时值。

        I D用于标识系统中的哪个线程当前拥有互斥对象,递归计数器用于指明该线程拥有互斥对象的次数。互斥对象有许多用途,属于最常用的内核对象之一。通常来说,它们用于保护由多个线程访问的内存块。如果多个线程要同时访问内存块,内存块中的数据就可能遭到破坏。互斥对象能够保证访问内存块的任何线程拥有对该内存块的独占访问权,这样就能够保证数据的完整性。

        互斥对象的使用规则如下:

        • 如果线程I D0(这是个无效I D) ,互斥对象不被任何线程所拥有,并且发出该互斥对象

    的通知信号。

        • 如果I D是个非0数字,那么一个线程就拥有互斥对象,并且不发出该互斥对象的通知信

    号。

        • 与所有其他内核对象不同, 互斥对象在操作系统中拥有特殊的代码,允许它们违反正常

    的规则(后面将要介绍这个异常情况)

        若要使用互斥对象,必须有一个进程首先调用C r e a t e M u t e x,以便创建互斥对象:

    HANDLE CreateMutex(

        PSECURITY_ATTRIBUTES psa,

        BOOL fInitialOwner,

        PCTSTR pszName);

        p s ap s z N a m e参数在第3章中做过介绍。当然,通过调用 O p e n M u t e x,另一个进程可以获得它自己进程与现有互斥对象相关的句柄:

    HANDLE OpenMutex(

        DWORD fdwAccess,

        BOOL bInheritHandle,

        PCTSTR pszName

    );

        f I n i t i a l O w n e r参数用于控制互斥对象的初始状态。如果传递 FA L S E(这是通常情况下传递的值) ,那么互斥对象的I D和递归计数器均被设置为0。这意味着该互斥对象没有被任何线程所拥有,因此要发出它的通知信号。

        如果为f I n i t i a l O w n e r参数传递T R U E,那么该对象的线程I D被设置为调用线程的I D,递归计数器被设置为1。由于I D是个非0数字,因此该互斥对象开始时不发出通知信号。

       通过调用一个等待函数,并传递负责保护资源的互斥对象的句柄,线程就能够获得对共享资源的访问权。在内部,等待函数要检查线程的 I D,以了解它是否是0(互斥对象发出通知信号) 。如果线程I D0,那么该线程I D被设置为调用线程的I D,递归计数器被设置为 1,同时,调用线程保持可调度状态。

        如果等待函数发现I D不是0(不发出互斥对象的通知信号) ,那么调用线程便进入等待状态。

    系统将记住这个情况,并且在互斥对象的 I D重新设置为0时,将线程I D设置为等待线程的I D,将递归计数器设置为1,并且允许等待线程再次成为可调度线程。与所有情况一样,对互斥内核对象进行的检查和修改都是以原子操作方式进行的。

        对于互斥对象来说,正常的内核对象的已通知和未通知规则存在一个特殊的异常情况。比如说,一个线程试图等待一个未通知的互斥对象。在这种情况下,该线程通常被置于等待状态。然而,系统要查看试图获取互斥对象的线程的I D是否与互斥对象中记录的线程I D相同。如果两个线程I D相同,即使互斥对象处于未通知状态,系统也允许该线程保持可调度状态。我们不认为该“异常”行为特性适用于系统中的任何地方的其他内核对象。每当线程成功地等待互斥对象时,该对象的递归计数器就递增。若要使递归计数器的值大于 1,唯一的方法是线程多次等待相同的互斥对象,以便利用这个异常规则。

        一旦线程成功地等待到一个互斥对象,该线程就知道它已经拥有对受保护资源的独占访问权。试图访问该资源的任何其他线程(通过等待相同的互斥对象)均被置于等待状态中。当目前拥有对资源的访问权的线程不再需要它的访问权时,它必须调用 R e l e a s e M u t e x函数来释放该互斥对象:

    BOOL ReleaseMutex(HANDLE hMutex);

        该函数将对象的递归计数器递减 1。如果线程多次成功地等待一个互斥对象,在互斥对象的递归计数器变成0之前,该线程必须以同样的次数调用 R e l e a s e M u t e x函数。当递归计数器到达0时,该线程I D也被置为0,同时该对象变为已通知状态。

        当该对象变为已通知状态时,系统要查看是否有任何线程正在等待互斥对象。如果有,系统将“按公平原则”选定等待线程中的一个,为它赋予互斥对象的所有权。当然,这意味着线程I D被设置为选定的线程的I D,并且递归计数器被置为1。如果没有其他线程正在等待互斥对象,那么该互斥对象将保持已通知状态,这样,等待互斥对象的下一个线程就立即可以得到互斥对象。

    9.6.1 释放问题

        互斥对象不同于所有其他内核对象,因为互斥对象有一个“线程所有权”的概念。本章介绍的其他内核对象中,没有一种对象能够记住哪个线程成功地等待到该对象,只有互斥对象能够对此保持跟踪。互斥对象的线程所有权概念是互斥对象为什么会拥有特殊异常规则的原因,这个异常规则使得线程能够获取该互斥对象,尽管它没有发出通知。

        这个异常规则不仅适用于试图获取互斥对象的线程, 而且适用于试图释放互斥对象的线程。当一个线程调用R e l e a s e M u t e x函数时,该函数要查看调用线程的I D是否与互斥对象中的线程I D相匹配。如果两个I D相匹配,递归计数器就会像前面介绍的那样递减。如果两个线程的 I D不匹配,那么R e l e a s e M u t e x函数将不进行任何操作,而是将FA L S E(表示失败)返回给调用者。此

    时调用G e t L a s t E r r o r,将返回E R R O R _ N O T _ O W N E R(试图释放不是调用者拥有的互斥对象) 。

        因此,如果在释放互斥对象之前,拥有互斥对象的线程终止运行(使用 E x i t T h r e a dTe r m i n a t e T h r e a dE x i t P r o c e s sTe r m i n a t e P r o c e s s函数) ,那么互斥对象和正在等待互斥对象的其他线程将会发生什么情况呢?答案是,系统将把该互斥对象视为已经被放弃——拥有互斥对象的线程决不会释放它,因为该线程已经终止运行.

        由于系统保持对所有互斥对象和线程内核对象的跟踪,因此它能准确的知道互斥对象何时被放弃。当一个互斥对象被放弃时,系统将自动把互斥对象的 I D复置为0,并将它的递归计数器复置为0。然后,系统要查看目前是否有任何线程正在等待该互斥对象。如果有,系统将“公平地”选定一个等待线程,将 I D设置为选定的线程的I D,并将递归计数器设置为1,同时,选定的线程变为可调度线程。

        这与前面的情况相同,差别在于等待函数并不将通常的 WA I T _ O B J E C T _ 0值返回给线程。相反,等待函数返回的是特殊的 WA I T _ A B A N D O N E D值。这个特殊的返回值(它只适用于互斥对象)用于指明线程正在等待的互斥对象是由另一个线程拥有的,而这另一个线程已经在它完成对共享资源的使用前终止运行。这不是可以进入的最佳情况。新调度的线程不知道目前资源处于何种状态,也许该资源已经完全被破坏了。在这种情况下必须自己决定应用程序应该怎么办。

        在实际运行环境中,大多数应用程序从不明确检查 WA I T _ A B A N D O N E D返回值,因为线程很少是刚刚终止运行(上面介绍的情况提供了另一个例子,说明为什么决不应该调用Te r m i n a t e T h r e a d函数) 。

    9.6.2 互斥对象与关键代码段的比较

        就等待线程的调度而言,互斥对象与关键代码段之间有着相同的特性。但是它们在其他属性方面却各不相同。表9 - 1对它们进行了各方面的比较。

     

    9.7 线程同步对象速查表

    9 - 2所示的速查表综合列出了各种内核对象与线程同步之间的相互关系。

     

     

     

        互锁(用户方式)函数决不会导致线程变为非调度状态,它们会改变一个值并立即返回。

    9.8 其他的线程同步函数

        Wa i t F o r S i n g l e O b j e c tWa i t F o r M u l t i p l e O b j e c t s是进行线程同步时使用得最多的函数。但是,Wi n d o w s还提供了另外几个稍有不同的函数。如果理解了Wa i t F o r S i n g l e O b j e c tWa i t F o r M u l t i p l eO b j e c t s函数,那么要理解其他函数如何运行,就不会遇到什么困难。本节简单地介绍一些这样的函数。

    9.8.1 异步设备I / O

        异步设备I / O使得线程能够启动一个读操作或写操作,但是不必等待读操作或写操作完成。例如,如果线程需要将一个大文件装入内存,那么该线程可以告诉系统将文件装入内存。然后,当系统加载该文件时,该线程可以忙于执行其他任务,如创建窗口、对内部数据结构进行初始化等等。当初始化操作完成时,该线程可以终止自己的运行,等待系统通知它文件已经读取。

        设备对象是可以同步的内核对象,这意味着可以调用 Wa i t F o r S i n g l e O b j e c t函数,传递文件、套接字和通信端口的句柄。当系统执行异步 I / O时,设备对象处于未通知状态。一旦操作完成,系统就将对象的状态改为已通知状态,这样,该线程就知道操作已经完成。此时,该线程就可以继续运行。

    9.8.2 Wa i t F o r I n p u t I d l e

        线程也可以调用Wa i t F o r I n p u t I d l e来终止自己的运行:

     

        该函数将一直处于等待状态,直到 h P r o c e s s标识的进程在创建应用程序的第一个窗口的线程中已经没有尚未处理的输入为止。这个函数可以用于父进程。父进程产生子进程,以便执行某些操作。当父进程的线程调用C r e a t e P r o c e s s时,该父进程的线程将在子进程初始化时继续运行。父进程的线程可能需要获得子进程创建的窗口的句柄。如果父进程的线程想要知道子进程何时完成初始化,唯一的办法是等待,直到子进程不再处理任何输入为止。因此,当调用C r e a t e P r o c e s s后,父进程的线程就调用Wa i t F o r I n p u t I d l e

    当需要将击键输入纳入应用程序时,也可以调用 Wa i t F o r I n p u t I d l e。比如说,可以将下面的消息显示在应用程序的主窗口:

     

        这个序列将A l t + F, O发送给应用程序,对于大多数使用英语的应用程序来说,它从应用程序的文件菜单中选择 O p e n命令。该命令打开一个对话框,但是,在对话框出现以前,Wi n d o w s必须加载来自文件的对话框摸板,遍历摸板中的所有控件,并为每个摸板调用 C r e a t eWi n d o w。这可能需要花费一定的时间。因此,显示 W M _ K E Y *消息的应用程序可以调用Wa i t F o r I n p u t I d l eWa i t F o r l n p u tId l e将导致应用程序处于等待状态,直到对话框创建完成并准备接受用户的输入。这时,该应用程序可以将其他的击键输入纳入对话框及其控件,使它能够继续执行它需要的操作。

        编写1 6Wi n d o w s应用程序的编程人员常常要面对这个问题。应用程序想要将消息显示在窗口中,但是它并不确切知道窗口何时创建完成、作好接受消息的准备。 Wa i t F o r I n p u t I d l e函数解决了这个问题。

    9.8.3 MsgWa i t F o r M u l t i p l e O b j e c t s ( E x )

        线程可以调用M s g Wa i t F o r M u l t i p l e O b j e c t sM s g Wa i t F o r M u l t i p l e O b j e c t s E x函数,让线程等

    待它自己的消息:

     

        这些函数与Wa i t F o r M u l t i p l e O b j e c t s函数十分相似。差别在于它们允许线程在内核对象变成已通知状态或窗口消息需要调度到调用线程创建的窗口中时被调度。

        创建窗口和执行与用户界面相关的任务的线程,应该调用 M s g Wa i t F o r M u l t i p l e O b j e c t s E x函数,而不应该调用Wa i t F o r M u l t i p l e O b j e c t s函数,因为后面这个函数将使线程的用户界面无法对用户作出响应。该函数将在第2 7章中详细介绍。

    9.8.4 Wa i t F o r D e b u g E v e n t

        Wi n d o w s将非常出色的调试支持特性内置于操作系统之中。当调试程序启动运行时,它将自己附加给一个被调试程序。该调试程序只需闲置着,等待操作系统将与被调试程序相关的调试事件通知它。调试程序通过调用Wa i t F o r D e b u g E v e n t函数来等待这些事件的发生:

     

        当调试程序调用该函数时,调试程序的线程终止运行,系统将调试事件已经发生的情况通知调试程序,方法是允许调用的Wa i t F o r D e b u g E v e n t函数返回。p d e参数指向的结构在唤醒调试程序的线程之前由系统填入信息。该结构包含了关于刚刚发生的调试事件的信息。


    9.8.5 SingleObjectAndWa i t

        S i n g l e O b j e c t A n d Wa i t函数用于在单个原子方式的操作中发出关于内核对象的通知并等待另一个内核对象:

     

        当调用该函数时,h O b j e c t To S i g n a l参数必须标识一个互斥对象、信标对象或事件。任何其他 类型的 对象将 导致 该函数 返回 WA I T _ FA I L E D ,并使 G e t L a s t E r r o r函 数返 回E R R O R _ I N VA L I D _ H A N D L E。在内部,该函数将观察对象的类型,并分别运行 R e l e a s e M u t e xR e l e a s e S e m a p h o r e (其数量为1) R e s e t E v e n t中的相应参数。

        h O b j e c t To Wa i t O n参数用于标识下列任何一个内核对象:互斥对象、信标、事件、定时器、进程、线程、作业、控制台输入和修改通知。与平常一样, d w M i l l i s e c o n d s参数指明该函数为了等待该对象变为已通知状态,应该等待多长时间,而 f A l e r t a b l e标志则指明线程等待时该线程是否应该能够处理任何已经排队的异步过程调用。

        该函数返回下列几个值中的一个: WA I T _ O B J E C T _ 0WA I T _ T I M E O U TWA I T _ FA I L E DWA I T _ A B A N D O N E D(本章前面已经介绍)或WA I T _ I O _ C O M P L E T I O N

        该函数是对Wi n d o w s的令人欢迎的一个补充,原因有二。首先,因为常常需要通知一个对象,等待另一个对象,用一个函数来执行两个操作可以节省许多处理时间。每次调用一个函数,使线程从用户方式代码变成内核方式代码,大约需要运行 1 0 0 0C P U周期。例如,运行下面的代码至少需要2 0 0 0C P U周期:

    ReleaseMutex(hMutex);

    WaitForSingleObject(hEvent ,INFINITE);

        在高性能服务器应用程序中,S i g n a l O b j e c t A n d Wa i t函数能够节省大量的处理时间。

        第二,如果没有S i g n a l O b j e c t A n d Wa i t函数,一个线程将无法知道另一个线程何时处于等待状态。对于 P l u s e E v e n t之类的函数来说,知道这个情况是很有用的。本章前面讲过,P u l s e E v e n t函数能够通知一个事件,并且立即对它进行重置。如果目前没有任何线程等待该事件,那么就没有事件会抓住这个情况。曾经有人编写过类似下面的代码:

     

        一个工作线程负责运行一些代码,然后调用 S e t E v e n t,以指明这项工作已经完成。另一个线程负责执行下面的代码:

     

        这个工作线程的代码段设计得很差,因为它无法可靠地运行。当工作线程调用 S e t E v e n t之后,另一个线程可能立即醒来,并调用 P u l s e E v e n t。该工作线程不得不停止运行,没有机会从它对S e t E v e n t的调用中返回,更不要说调用 Wa i t F o r S i n g l e O b j e c t函数了。结果, h E v e n tM o r e Wo r k To B e D o n e事件的通知就完全被工作线程错过了。

        如果像下面所示的那样重新编写工作线程的代码,以便调用 S i n g l e O b j e c t A n d Wa i t函数,那么该代码就能够可靠地运行,因为通知和等待都能够以原子操作方式来进行:

     

        当非工作线程醒来时,它能够百分之百地确定工作线程正在等待 h E v e n t M o r e Wo r k To B eD o n e事件,因此能够确保看到产生该事件。

        Windows 98 Windows 98 没有这个函数的可以使用的实现代码。

  • 相关阅读:
    【JAVA】java 堆溢出分析
    【数据结构】链表的基本学习 实现
    【其他】博客园自定义样式指南
    【JAVA】一些好用的开源java类库
    【Shell】Shell 常用命令
    【Spring】Spring Boot 要点整理
    【数据库】Mysql 连接相关
    【Linux】Shell 参数解析
    Erlang 中类型转换
    erlang 中 maps 练习
  • 原文地址:https://www.cnblogs.com/csnd/p/12062223.html
Copyright © 2020-2023  润新知