前言

  若干种内核对象,包括进程,线程和作业。可以将所有这些内核对象用于同步目的。对于线程同步来说,这些内核对象中的每种对象都可以说是处于已通知或未通知的状态之中。这种状态的切换是由Microsoft为每个对象建立的一套规则来决定的。例如,进程内核对象总是在未通知状态中创建的。当进程终止运行时,操作系统自动使该进程的内核对象处于已通知状态。一旦进程内核对象得到通知,它将永远保持这种状态,它的状态永远不会改为未通知状态。
  当进程正在运行的时候,进程内核对象处于未通知状态,当进程终止运行的时候,它就变为已通知状态。进程内核对象中是个布尔值,当对象创建时,该值被初始化为FALSE(未通知状态)。当进程终止运行时,操作系统自动将对应的对象布尔值改为TRUE,表示该对象已经得到通知。
  如果编写的代码是用于检查进程是否仍在运行,那么只需要调用一个函数,让操作系统去检查进程对象的布尔值,这非常简单。你也可能想要告诉系统使线程进入等待状态,然后当布尔值从FALSE为TRUE时自动唤醒该线程。这样,你可以编写一个代码,在这个代码中,需要等待子进程终止运行的父进程中的线程只需要使自己进入睡眠状态,直到标识子进程的内核对象变为已通知状态即可。你将会看到,Microsoft的Windows提供了一些能够非常容易地完成这些操作的函数。
  刚才讲了Microsoft为进程内核对象定义了一些规则。实际上,线程内核对象也遵循同样的规则。即线程内核对象总是在未通知状态中创建。当线程终止运行时,操作系统会自动将线程对象的状态改为已通知状态。因此,可以将相同的方法用于应用程序,以确定线程是否不再运行。与进程内核对象一样,线程内核对象也可以处于已通知状态或未通知状态。下面的内核对象可以处于已通知状态或未通知状态:
  ■ 进程    ■ 文件修改通知
  ■ 线程    ■ 事件
  ■ 作业    ■ 可等待定时器

  ■ 文件    ■ 信标
  ■ 控制台输入 ■ 互斥对象

  线程可以使自己进入等待状态,直到一个对象变为已通知状态。注意,用于控制每个对象的已通知/未通知状态的规则要根据对象的类型而定。前面已经提到进程和线程对象的规则及作业的规则。
  本章将要介绍允许线程等待某个内核对象变为已通知状态所用的函数。然后我们将要讲述Wi n d o w s提供的专门用来帮助实现线程同步的各种内核对象、如事件、等待计数器,信标和互斥对象。
  当我最初开始学习这项内容时,我设想内核对象包含了一面旗帜(在空中飘扬的旗帜,不是耷拉下来的旗帜),这对我很有帮助。当内核对象得到通知时,旗帜升起来;当对象未得到通知时,旗帜就降下来(见图9 - 1)。
  当线程等待的对象处于未通知状态(旗帜降下)中时,这些线程不可调度。但是一旦对象变为已通知状态(旗帜升起),线程看到该标志变为可调度状态,并且很快恢复运行(见图9 - 2)。

  9-1视图9-1视图

  

1  等待函数

  等待函数可使线程自愿进入等待状态,直到一个特定的内核对象变为已通知状态为止。这些等待函数中最常用的是WaitForSingleObject:

DWORD WaitForSingleObject(
    HANDLE hObject,
    DWORD dwMilliseconds);

  当线程调用该函数时,第一个参数hObject标识一个能够支持被通知/未通知的内核对象(前面列出的任何一种对象都适用)。第二个参数dwMilliseconds允许该线程指明,为了等待该对象变为已通知状态,它将等待多长时间。
  调用下面这个函数将告诉系统,调用函数准备等待到hProcess句柄标识的进程终止运行为止:

WaitForSingleObject(hProcess, INFINITE);

  第二个参数告诉系统,调用线程愿意永远等待下去(无限时间量),直到该进程终止运行。

  通常情况下, INFINITE是作为第二个参数传递给WaitForSingleObject的,不过也可以传递任何一个值(以毫秒计算)。顺便说一下,INFINITE已经定义为0xFFFFFFFF(或-1)。当然,传递INFINITE有些危险。如果对象永远不变为已通知状态,那么调用线程永远不会被唤醒,它将永远处于死锁状态,不过,它不会浪费宝贵的CPU时间。下面是如何用一个超时值而不是INFINITE来调用WaitForSingleObject的例子:

DWORD dw = WaitForSingleObject(hProcess, 5000);
switch(dw){
    case WAIT_OBJECT_0:
        // The process terminated
        break;
    case WAIT_TIMEOUT:
        // The process did not terminate within 5000 milliseconds
        break;
    case WAIT_FAILED:
        // Bad call to function (invalid handle?)
        break;
}

  上面这个代码告诉系统,在特定的进程终止运行之前,或者在5000ms时间结束之前,调用线程不应该变为可调度状态。因此,如果进程终止运行,那么这个函数调用将在不到5000ms的时间内返回,如果进程尚未终止运行,那么它在大约5000ms时间内返回。注意,不能为dwMilliseconds传递0。如果传递了0,WaitForSingleObject函数将总是立即返回。
  WaitForSingleObject的返回值能够指明调用线程为什么再次变为可调度状态。如果线程等待的对象变为已通知状态,那么返回值是WAIT_OBJECT_0。如果设置的超时已经到期,则返回值是WAIT_TIMEOUT。如果将一个错误的值(如一个无效句柄)传递给WaitForSingleObject,那么返回值将是WAIT_FAILED(若要了解详细信息,可调用GetLastError)。
  下面这个函数WaitForMultipleObjects与WaitForSingleObject函数很相似,区别在于它允许调用线程同时查看若干个内核对象的已通知状态:

DWORD WaitForMultipleObjects(
    DWORD dwCount,
    CONST HANDLE* phObjects,
    BOOL fWaitAll,
    DWORD dwMilliseconds);

  dwCounts参数用于指明想要让函数查看的内核对象的数量。这个值必须在1与MAXIMUM_WAITOBJECTS(在Windows头文件中定义为64)之间。phObjects参数是指向内核对象句柄的数组的指针。

  可以以两种不同的方式来使用WaitForMultipleObjects函数。一种方式是让线程进入等待状态,直到指定内核对象中的任何一个变为已通知状态。另一种方式是让线程进入等待状态,直到所有指定的内核对象都变为已通知状态。fWaitAll参数告诉该函数,你想要让它使用何种方式。如果为该参数传递TRUE,那么在所有对象变为已通知状态之前,该函数将不允许调用线程运行。

  dwMilliseconds参数的作用与它在Wa WaitForSingleObject中的作用完全相同。如果在等待的时候规定的时间到了,那么该函数无论如何都会返回。同样,通常为该参数传递INFINITE,但是在编写代码时应该小心,以避免出现死锁情况。
  WaitForMultipleObjects函数的返回值告诉调用线程,为什么它会被重新调度。可能的返回值是WAIT_FAILED和WAIT_TIMEOUT,这两个值的作用是很清楚的。如果为fWaitAll参数传递TRUE,同时所有对象均变为已通知状态,那么返回值是WAIT_OBJECT_0。如果为fWaitAll传递FALSE,那么一旦任何一个对象变为已通知状态,该函数便返回。在这种情况下,你可能想要知道哪个对象变为已通知状态。返回值是WAIT_OBJECT_0与(WAIT_OBJECT_0 + dwCount - 1)之间的一个值。换句话说,如果返回值不是WAIT_TIMEOUT,也不是WAIT_FAILED,那么应该从返回值中减去WAIT_OBJECT_0。产生的数字是作为第二个参数传递给WaitForMultipleObjects的句柄数组中的索引。该索引说明哪个对象变为已通知状态。下面是说明这一情况的一些示例代码:

HANDLE h[3];
h[0] = hProcess1;
h[1] = hProcess2;
h[2] = hProcess3;

DWORD dw = WaitForMultipleObjects(3, h, FALSE, 5000);
switch(dw){
    case WAIT_FAILED:
        // Bad call to function (invalid handle?)
        break;
    case WAIT_TIMEOUT:
        // None of the objects became signaled within 5000 milliseconds
        break;
    case WAIT_OBJECT_0 + 0:
        // The process identified by h[0](hProcess1)  terminated
        break;
    case WAIT_OBJECT_0 + 1:
        // The process identified by h[0](hProcess2)  terminated
        break;
    case WAIT_OBJECT_0 + 2:
        // The process identified by h[0](hProcess3)  terminated
        break;
}

  如果为fWaitAll参数传递FALSE,WaitForMultipleObjects就从索引0开始向上对句柄数组进行扫描,同时已通知的第一个对象终止等待状态。这可能产生一些你不希望有的结果。例如,通过将3个进程句柄传递给该函数,你的线程就会等待3个子进程终止运行。如果数组中索引为0的进程终止运行,WaitForMultipleObjects就会返回。这时该线程就可以做它需要的任何事情,然后循环反复,等待另一个进程终止运行。如果该线程传递相同的3个句柄,该函数立即再次返回WAIT_OBJECT_0。除非删除已经收到通知的句柄,否则代码就无法正确地运行。

2  成功等待的副作用

  对于有些内核对象来说,成功地调用WaitForSingleObject和WaitForMultipleObjects,实际上会改变对象的状态。成功地调用是指函数发现对象已经得到通知并且返回一个相对于WA I T O B J E C T 0的值。如果函数返回WA I T T I M E O U T或WA I T FA I L E D,那么调用就没有成功。如果函数调用没有成功,对象的状态就不可能改变。
  当一个对象的状态改变时,我称之为成功等待的副作用。例如,有一个线程正在等待自动清除事件对象(本章后面将要介绍)。当事件对象变为已通知状态时,函数就会发现这个情况,并将WA I T O B J E C T 0返回给调用线程。但是就在函数返回之前,该事件将被置为未通知状态,这就是成功等待的副作用。
  这个副作用将用于自动清除内核对象,因为它是M i c r o s o f t为这种类型的对象定义的规则之一。其他对象拥有不同的副作用,而有些对象则根本没有任何副作用。进程和线程内核对象就根本没有任何副作用,也就是说,在这些对象之一上进行等待决不会改变对象的状态。由于本章要介绍各种不同的内核对象,因此我们将要详细说明它们的成功等待的副作用。
  究竟是什么原因使得WaitForMultipleObjects函数如此有用呢,因为它能够以原子操作方式来执行它的所有操作。当一个线程调用WaitForMultipleObjects函数时,该函数能够测试所有对象的通知状态,并且能够将所有必要的副作用作为一项操作来执行。
  让我们观察一个例子。两个线程以完全相同的方式来调用WaitForMultipleObjects:

HANDLE h[2];
h[0] = hAutoResetEvent1;    // Initially nonsignaled
h[1] = hAutoResetEvent2;    // Initially nonsignaled
WaitForMultipleObjects(2, h, TRUE, INFINITE);

  当WaitForMultipleObjects函数被调用时,两个事件都处于未通知状态,这就迫使两个线程都进入等待状态。然后hAutoResetEvent1对象变为已通知状态。两个线程都发现,该事件已经变为已通知状态,但是它们都无法被唤醒,因为hAutoResetEvent2仍然处于未通知状态。由于两个线程都没有等待成功,因此没有对hAutoResetEvent1对象产生任何副作用。
  接着,hAutoResetEvent2变为已通知状态。这时,两个线程中的一个发现,两个对象都变为已通知状态。等待取得了成功,两个事件对象均被置为未通知状态,该线程变为可调度的线程。但是另一个线程的情况如何呢?它将继续等待,直到它发现两个事件对象都处于已通知状态。尽管它原先发现hAutoResetEvent1处于已通知状态,但是现在它将该对象视为未通知状态。
  前面讲过,有一个重要问题必须注意,即WaitForMultipleObjects是以原子操作方式运行的。当它检查内核对象的状态时,其他任何线程都无法背着对象改变它的状态。这可以防止出现死锁情况。试想,如果一个线程看到hAutoResetEvent1已经得到通知并将事件重置为未通知状态,然后,另一个线程发现hAutoResetEvent2已经得到通知并将该事件重置为未通知状态,那么这两个线程均将被冻结:一个线程将等待另一个线程已经得到的对象,另一个线程将等待该线程
已经得到的对象。WaitForMultipleObjects能够确保这种情况永远不会发生。
这会产生一个非常有趣的问题,即如果多个线程等待单个内核对象,那么当该对象变成已通知状态时,系统究竟决定唤醒哪个线程呢? Microsoft对这个问题的正式回答是:“算法是公平的。”Microsoft不想使用系统使用的内部算法。它只是说该算法是公平的,这意味着如果多个线程正在等待,那么每当对象变为已通知状态时,每个线程都应该得到它自己的被唤醒的机会。

  这意味着线程的优先级不起任何作用,即高优先级线程不一定得到该对象。这还意味着等待时间最长的线程不一定得到该对象。同时得到对象的线程有可能反复循环,并且再次得到该对象。但是,这对于其他线程来说是不公平的,因此该算法将设法防止这种情况的出现。但是这不一定做得到。
  在实际操作中, Microsoft使用的算法是常用的“先进先出”的方案。等待了最长时间的线程将得到该对象。但是系统中将会执行一些操作,以便改变这个行为特性,使它不太容易预测。这就是为什么Microsoft没有明确说明该算法如何起作用的原因。操作之一是让线程暂停运行。如果一个线程等待一个对象,然后该线程暂停运行,那么系统就会忘记该线程正在等待该对象。这是一个特性,因为没有理由为一个暂停运行的线程进行调度。当后来该线程恢复运行时,系统将认为该线程刚刚开始等待该对象。
  当调试一个进程时,只要到达一个断点,该进程中的所有线程均暂停运行。因此,调试一个进程会使“先进先出”的算法很难预测其结果,因为线程常常暂停运行,然后再恢复运行。

3  时间内核对象

  在所有的内核对象中,事件内核对象是个最基本的对象。它们包含一个使用计数(与所有内核对象一样),一个用于指明该事件是个自动重置的事件还是一个人工重置的事件的布尔值,另一个用于指明该事件处于已通知状态还是未通知状态的布尔值。
  事件能够通知一个操作已经完成。有两种不同类型的事件对象。一种是人工重置的事件,另一种是自动重置的事件。当人工重置的事件得到通知时,等待该事件的所有线程均变为可调度线程。当一个自动重置的事件得到通知时,等待该事件的线程中只有一个线程变为可调度线程。
  当一个线程执行初始化操作,然后通知另一个线程执行剩余的操作时,事件使用得最多。事件初始化为未通知状态,然后,当该线程完成它的初始化操作后,它就将事件设置为已通知状态。这时,一直在等待该事件的另一个线程发现该事件已经得到通知,因此它就变成可调度线程。这第二个线程知道第一个线程已经完成了它的操作。下面是CreateEvent函数,用于创建事件内核对象:

HANDLE CreateEvent(
    PSECURITY_ATTRIBUTES psa,
    BOOL fManualReset,
    BOOL fInitialState,
    PCTSTR pszName);

参数编辑

lpEventAttributes[输入]
  一个指向SECURITY_ATTRIBUTES结构的指针,确定返回的句柄是否可被子进程继承。如果lpEventAttributes是NULL,此句柄不能被继承。Windows NT/2000:lpEventAttributes的结构中的成员为新的事件指定了一个安全符。如果lpEventAttributes是NULL,事件将获得一个默认的安全符。
bManualReset[输入]
  指定将事件对象创建成手动复原还是自动复原。如果是TRUE,那么必须用ResetEvent函数来手工将事件的状态复原到无信号状态。如果设置为FALSE,当一个等待线程被释放以后,系统将会自动将事件状态复原为无信号状态。
bInitialState[输入]
  指定事件对象的初始状态。如果为TRUE,初始状态为有信号状态(已通知状态);否则为无信号状态(未通知状态)。
lpName[输入]
  指定事件的对象的名称,是一个以0结束的字符串指针。名称的字符格式限定在MAX_PATH之内。名字是对大小写敏感的。

 

  当系统创建事件对象后,CreateEvent就将与进程相关的句柄返回给事件对象。其他进程中的线程可以获得对该对象的访问权,方法是使用在pszName参数中传递的相同值,使用继承性,使用DuplicateHandle函数等来调用CreateEvent,或者调用OpenEvent ,在pszName参数中设定一个与调用CreateEvent时设定的名字相匹配的名字:

HANDLE OpenEvent(      //The OpenEvent function returns a handle to an existing named event object. 
    DWORD fdwAccess,
    BOOL fInhert,
    PCTSTR pszName);

  与所有情况中一样,当不再需要事件内核对象时,应该调用CloseHandle函数。
  一旦事件已经创建,就可以直接控制它的状态。当调用SetEvent时,可以将事件改为已通知状态:

BOOL SetEvent(HANDLE hEvent);

  当调用ResetEvent函数时,可以将该事件改为未通知状态:

BOOL ResetEvent(HANDLE hEvent);

  就是这么容易。Microsoft为自动重置的事件定义了应该成功等待的副作用规则,即当线程成功地等待到该对象时,自动重置的事件就会自动重置到未通知状态。这就是自动重置的事件如何获得它们的名字的方法。通常没有必要为自动重置的事件调用ResetEvent函数,因为系统会自动对事件进行重置。但是, Microsoft没有为人工重置的事件定义成功等待的副作用。让我们观察一个简单的例子,以便说明如何使用事件内核对象对线程进行同步。下面就是这个代码:

// Create a global handle to a manual-reset, nonsignaled event
HANDLE g_hEvent;

int WINAPI WinMain(...){

    // Create the manual-reset, nonsignaled event
    g_hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);

    // Spawn 3 new threads
    HANDLE hThread[3];
    DWORD dwThreadID;
    hThread[0] = _beginthreadex(NULL, 0, WordCount, NULL, 0, &dwThreadID);
    hThread[1] = _beginthreadex(NULL, 0, SpellCheck, NULL, 0, &dwThreadID);
    hThread[2] = _beginthreadex(NULL, 0, GrammarCheck, NULL, 0, &dwThreadID);

    OpenFileAndReadContentsIntoMemory(...);

    // Allow all 3 threads to access the memory
    SetEvent(g_hEvent);
    ...
}

DWORD WINAPI WordCount(PVOID pvParam){
    // Wait until the file's data is in memory
    WaitForSingleObject(g_hEvent, INFINITE);

    // Access the memory block
    ...
    return 0;
}

DWORD WINAPI SpellCheck(PVOID pvParam){
    // Wait until the file's data is in memory
    WaitForSingleObject(g_hEvent, INFINITE);

    // Access the memory block
    ...
    return 0;
}

DWORD WINAPI GrammarCheck(PVOID pvParam){
    // Wait until the file's data is in memory
    WaitForSingleObject(g_hEvent, INFINITE);

    // Access the memory block
    ...
    return 0;
}

  当这个进程启动时,它创建一个人工重置的未通知状态的事件,并且将句柄保存在一个全局变量中。这使得该进程中的其他线程能够非常容易地访问同一个事件对象。现在3个线程已经产生。这些线程要等待文件的内容读入内存,然后每个线程都要访问它的数据。一个线程进行单词计数,另一个线程运行拼写检查器,第三个线程运行语法检查器。这3个线程函数的代码的开始部分都相同,每个函数都调用WaitForSingleObject,这将使线程暂停运行,直到文件的内容由主线程读入内存为止。
  一旦主线程将数据准备好,它就调用SetEvent,给事件发出通知信号。这时,系统就使所有这3个辅助线程进入可调度状态,它们都获得了CPU时间,并且可以访问内存块。注意,这3个线程都以只读方式访问内存。这就是所有3个线程能够同时运行的唯一原因。还要注意,如何计算机上配有多个CPU,那么所有3个线程都能够真正地同时运行,从而可以在很短的时间内完成大量的操作。
  如果你使用自动重置的事件而不是人工重置的事件,那么应用程序的行为特性就有很大的差别。当主线程调用SetEvent之后,系统只允许一个辅助线程变成可调度状态。同样,也无法保证系统将使哪个线程变为可调度状态。其余两个辅助线程将继续等待。
  已经变为可调度状态的线程拥有对内存块的独占访问权。让我们重新编写线程的函数,使得每个函数在返回前调用SetEvent函数(就像WinMain函数所做的那样)。这些线程函数现在变成下面的形式:

DWORD WINAPI WordCount(PVOID pvParam){
    // Wait until the file's data is in memory
    WaitForSingleObject(g_hEvent, INFINITE);

    // Access the memory block
    ...
    SetEvent(g_hEvent);
    return 0;
}

DWORD WINAPI SpellCheck(PVOID pvParam){
    // Wait until the file's data is in memory
    WaitForSingleObject(g_hEvent, INFINITE);

    // Access the memory block
    ...
    SetEvent(g_hEvent);
    return 0;
}

DWORD WINAPI GrammarCheck(PVOID pvParam){
    // Wait until the file's data is in memory
    WaitForSingleObject(g_hEvent, INFINITE);

    // Access the memory block
    ...
    SetEvent(g_hEvent);
    return 0;
}

  当线程完成它对数据的专门传递时,它就调用SetEvent函数,该函数允许系统使得两个正在等待的线程中的一个成为可调度线程。同样,我们不知道系统将选择哪个线程作为可调度线程,但是该线程将进行它自己的对内存块的专门传递。当该线程完成操作时,它也将调用SetEvent函数,使第三个即最后一个线程进行它自己的对内存块的传递。注意,当使用自动重置事件时,如果每个辅助线程均以读/写方式访问内存块,那么就不会产生任何问题,这些线程将不再被要求将数据视为只读数据。这个例子清楚地展示出使用人工重置事件与自动重置事件之间的差别。为了完整起见,下面再介绍一个可以用于事件的函数:

BOOL PulseEvent(HANDLE hEvent);
/******************************************************/
The PulseEvent function provides a single operation that sets (to signaled) the state of the specified event object and then resets it (to nonsignaled) after releasing the appropriate number of waiting threads. 

BOOL PulseEvent(
  HANDLE hEvent   // handle to event object
);
 
Parameters
hEvent 
Handle to the event object. The CreateEvent or OpenEvent function returns this handle. 
Windows NT: The handle must have EVENT_MODIFY_STATE access. 

Return Values
If the function succeeds, the return value is nonzero.
If the function fails, the return value is zero. To get extended error information, call GetLastError. 
/******************************************************/

  PulseEvent函数使得事件变为已通知状态,然后立即又变为未通知状态,这就像在调用SetEvent后又立即调用ResetEvent函数一样。如果在人工重置的事件上调用PulseEvent函数,那么在发出该事件时,等待该事件的任何一个线程或所有线程将变为可调度线程。如果在自动重置事件上调用PulseEvent函数,那么只有一个等待该事件的线程变为可调度线程。如果在发出事件时没有任何线程在等待该事件,那么将不起任何作用。
  PulseEvent函数并不非常有用。实际上我在自己的应用程序中从未使用它,因为根本不知道什么线程将会看到事件的发出并变成可调度线程。由于在调用PulseEvent时无法知道任何线程的状态,因此该函数并不那么有用。我相信在有些情况下,虽然PulseEvent函数可以方便地供你使用,但是你根本想不起要去使用它。

4  等待定时器内核对象

 

未完待续。。。

 

posted on 2015-12-23 11:31  超酷小子  阅读(500)  评论(0编辑  收藏  举报