停!谁在那儿?






4.78/5 (40投票s)
演示了多线程应用程序中用于线程同步的各种等待函数
引言
在软件开发人员的职业生涯中,肯定都会编写、接触或至少听说过多线程应用程序。编写一个行为良好的多线程应用程序中最困难的任务之一是其中创建的各个线程之间的同步。所有版本的 Windows 操作系统都提供同步对象来协助完成上述任务。事件 (Events)、互斥体 (Mutexes) 和信号量 (Semaphores) 是 Windows 提供的一些同步对象。
同步的一个常见场景是,一个完成工作的线程需要等待其他一个或多个线程完成它们的执行。等待的线程通常是应用程序的主线程,但也可能是任何其他线程。
有各种“等待函数”可用于实现上述等待。在本文中,我将尝试解释每种可用的等待函数及其之间的区别。
先决条件
读者应具备扎实的 Windows 编程知识,并对多线程应用程序的工作原理有一个大致的了解。由于示例程序是用 Visual C++ 和 MFC 编写的,因此熟练掌握 VC++ 和 MFC 知识也是必不可少的。
一般描述
所有等待函数工作的基本原理是等待一个对象句柄。这些函数可以等待诸如事件、互斥体、信号量、线程、进程、可等待计时器等对象的句柄。还可以指定一个超时值,这是函数等待的最大时间。
所有等待函数都会等待对象句柄或句柄,直到满足某些指定的条件。所有这些函数的基本条件是它所等待的对象的信号状态和超时值。调用线程会一直等待,直到对象进入信号状态并且超时时间已过。线程处于等待状态时,不会占用处理器时间。
对象如何变为信号状态取决于所处理对象的类型。例如,通过调用 SetEvent
API 来设置事件对象为信号状态;当互斥体对象不被任何线程拥有时,它就变为信号状态;通过调用 SetWaitableTimer
API 来设置可等待计时器为信号状态;当线程函数返回或调用 ExitThread
或 TerminateThread
API 时,线程变为信号状态。
传递给等待函数的超时值以毫秒为单位。如果对象未进入信号状态且超时时间已过,则函数返回。如果函数需要永远等待对象进入信号状态,可以将常量 INFINITE
传递给超时值。INFINITE
实际上定义为十六进制值 FFFFFFFF,转换后大约等于 50 天,我认为这是一个函数可以等待的相当长的时间。
除了这两个条件外,一些函数还有其他一些条件,将在后面的章节中介绍。这些等待函数的返回值决定了函数返回的原因。换句话说,函数的返回值确认了它满足的条件。
示例应用程序中使用的缩写
WSO | 等待单个对象 |
WMO | 等待多个对象 |
MWMO | 消息等待多个对象 |
WSOE | 等待单个对象 Ex |
WMOE | 等待多个对象 Ex |
MWMOE | 消息等待多个对象 Ex |
SOAW | 信号对象并等待 |
关于示例应用程序
在开始解释等待函数之前,我需要向您提供一些关于示例应用程序是如何编写的、如何使用它以及鼓励读者做出哪些更改以更好地理解各种等待函数的工作原理的信息。
使用该应用程序只需两次鼠标点击。单选按钮用于选择要使用的等待函数,而“调用”按钮实际执行操作。所有线程的状态始终显示在旁边。函数返回后,等待函数等待的时间(以秒为单位)也会显示出来。
该应用程序包含一个名为 Constants.h 的文件,其中包含一些读者可以随意更改的常量值。修改后,应用程序将在重新编译后根据新值运行。每个等待函数的常量都已在 Constants.h 文件中的独立部分给出。例如,WaitForSingleObject
API 的常量如下所示:
//####################### Constants for WaitForSingleObject
#define WSO_SLEEPTIME 3000
#define WSO_TIMEOUT INFINITE
在实现文件 WaitFunctionsDlg.cpp 中,处理特定等待函数的函数也分别放在了不同的部分。例如,处理 WaitForSingleObject
API 的函数位于如下所示的部分:
//## Functions for handling the WaitForSingleObject request
//# Thread Function
DWORD WINAPI WaitForSingleObjectProc(LPVOID)
//# Helper Function
void CWaitFunctionsDlg::InvokeWaitForSingleObject()
为了清晰起见,线程函数和每个等待函数的辅助函数是分开编写的。这可能会引入大量的代码冗余,但这是故意的,以便读者可以一次专注于一个等待函数,避免在大量“if”条件等之间导航。
所有线程入口点函数都遵循 xxxxProc 的命名约定,其中 xxxx 是等待函数的名称;所有辅助函数都遵循 Invokexxxx 的命名约定,其中 xxxx 同样是等待函数的名称。
说了这么多,让我们开始讲等待函数。
等待函数
WaitForSingleObject
这是所有等待函数中最简单的。它只指定了两个基本条件:一个对象句柄和一个超时值。调用此函数的线程将阻塞,直到函数返回。当对象进入信号状态或超时时间到期时,函数将返回。
在示例应用程序中,主线程中的函数 InvokeWaitForSingleObject
创建另一个线程并等待该线程句柄,如下所示:
m_ahThread[0] = CreateThread(0, 0,
WaitForSingleObjectProc, 0, 0, 0);
WaitForSingleObject(m_ahThread[0], WSO_TIMEOUT);
新创建的线程仅休眠 WSO_SLEEPTIME
指定的时间间隔,然后返回。在线程函数返回之前,主线程(等待其他线程的句柄)将阻塞在等待调用处,并且不会响应任何消息。尝试拖动对话框即可验证这一点。由于该函数会完全阻塞线程,因此不建议从用户界面线程调用它。
WaitForMultipleObjects
此函数类似于 WaitForSingleObject
函数,但它等待一个或多个线程完成,而不是只等待一个线程。同样,调用此函数的线程将阻塞直到函数返回。因此,我前面说过,不建议从用户界面线程调用它。
示例应用程序创建了多个具有 3 种不同超时值的线程。第一个线程创建时使用的超时值在 WMO_MINSLEEPTIME
中指定,最后一个线程创建时使用的超时值在 WMO_MAXSLEEPTIME
中指定,所有其他线程创建时使用的超时值在 WMO_MIDSLEEPTIME
中指定。
WaitForMultipleObjects
在超时到期时返回,或者(取决于 WMO_WAITFORALL
的值)当一个或所有对象进入信号状态时返回。如果 WMO_WAITFORALL
的值为 TRUE
,则所有对象都必须进入信号状态函数才能返回;如果为 FALSE
,则第一个进入信号状态的对象会导致函数返回。
MsgWaitForMultipleObjects
此函数类似于 WaitForMultipleObjects
函数,但它接受一个附加参数,该参数指定一种事件类型。事件类型可以是鼠标事件、键盘事件或计时器事件等。MSDN 中记录的各种事件类型都以 QS_
开头。此事件类型是等待函数返回的另一个条件。
例如,如果将 QS_HOTKEY
指定为事件类型,等待函数将检查调用线程的消息队列中是否已发布 WM_HOTKEY
消息。
在示例应用程序中,创建了多个具有 3 种不同超时值的线程,这与 WaitForMultipleObjects
的情况类似。如果 MWMO_WAITFORALL
的值为 TRUE
,则函数会一直等待,直到 MWMO_TIMEOUT
中指定的超时值到期,**或者** 直到所有对象都进入信号状态**并且**指定的事件发生。如果 MWMO_WAITFORALL
的值为 FALSE
,则函数会一直等待,直到 MWMO_TIMEOUT
中指定的超时值到期,**或者** 直到任何一个对象进入信号状态,**或者** 指定的事件发生。事件类型在 MWMO_EVENTS
中指定,并且可以使用管道(“|”)运算符将多个事件类型组合到其中。
如果 MWMO_TIMEOUT
、MWMO_WAITFORALL
和 MWMO_EVENTS
的值分别为 INFINITE
、TRUE
和 QS_KEY
,则函数仅在所有对象进入信号状态**并且**键盘消息进入调用线程的消息队列时才返回。
WaitForSingleObjectEx
此函数类似于 WaitForSingleObject
函数,但它接受一个额外的布尔参数,该参数的值决定调用线程是否处于可警报等待状态。
如果此布尔参数设置为 TRUE
,则每当队列中出现完成例程或异步过程调用 (APC) 时,函数就会返回。当调用 QueueUserAPC
API 时会排队一个 APC,当 ReadFileEx
或 WriteFileEx
API 完成时会发生完成例程排队。当调用 ReadFileEx
或 WriteFileEx
API 时,会将完成函数名称作为参数传递。当实际的读写完成时,等待函数会返回,并且如果线程处于可警报等待状态,系统会调用完成函数。
让我们举一个例子来更好地理解可警报等待状态的含义。假设您正在使用命名管道或邮件槽进行进程间通信 (IPC)。此外,假设客户端应用程序需要等待一个线程完成。同时,每当消息到达管道时,都必须执行一些操作。这可以通过使用 WaitForSingleObjectEx
API 来实现,将线程句柄作为第一个参数,并将可警报参数设置为 TRUE。
在示例应用程序中,正在执行异步文件写入操作。WaitForSingleObjectEx
API 会在一个循环中调用,直到超时到期或对象进入信号状态。如果 WSOE_ALERTABLE
的值设置为 TRUE
,则每次文件写入完成时,等待函数都会返回并调用完成例程。然后执行另一个异步写入,并再次调用等待函数。完成例程简单地推进一个进度控件。如果 WSOE_ALERTABLE
的值设置为 FALSE
,则 WaitForSingleObjectEx
API 的工作方式与 WaitForSingleObject
API 相同。
WaitForMultipleObjectsEx
此函数类似于 WaitForMultipleObjects
函数,但它接受一个额外的可警报参数,就像 WaitForSingleObjectEx
API 一样。此函数等待直到超时到期,或者直到一个或多个对象进入信号状态,或者直到完成例程被排队。
在示例应用程序中,它的工作方式类似于 WaitForMultipleObjects
API 的情况,不同的是正在进行异步文件操作,其完成例程会推进进度控件。
MsgWaitForMultipleObjectsEx
此函数是前面介绍的所有其他等待函数的组合。它类似于 MsgWaitForMultipleObjects
API,只是增加了一个可警报参数。此函数等待直到超时到期,或者直到一个或多个对象进入信号状态,或者直到指定的事件发生,或者直到完成例程被排队。
此函数可以替代上述任何一个等待函数。例如,在示例应用程序中,将 MWMOE_EVENTS
设置为 0,将 MWMOE_ALERTABLE
设置为 FALSE
,将 MWMOE_WAITFORALL
设置为 TRUE
,并只指定一个对象句柄,其功能将与 WaitForSingleObject
API 完全相同。
SignalObjectAndWait
此函数类似于 WaitForSingleObjectEx
API,但它接受一个额外的第一个参数,该参数是一个信号量、互斥体或事件的句柄。调用此 API 时,它首先将传递为第一个参数的对象设置为信号状态,然后其行为与 WaitForSingleObjectEx
API 相同。
例如,如果传递给 SignalObjectAndWait
API 的第一个参数是事件对象的句柄,那么我们可以说它实际上是将 SetEvent
API 和 WaitForSingleObjectEx
API 合二为一。
在示例应用程序中,一旦选中此单选按钮(甚至在单击“调用”按钮之前),就会创建一个线程。该线程使用 WaitForSingleObject
API 等待一个事件。调用 SignalObjectAndWait
API 时,它首先发出线程正在等待的事件信号。其余部分与 WaitForSingleObjectEx
API 的情况完全相同。
其他函数
还有一些其他等待函数与多线程和同步无关。我将它们包含在这篇文章中,仅仅是因为它们属于同一类函数,即等待或阻塞的函数。这些函数不包含在本文附带的示例程序中。
Sleep
这是最基础的等待函数。它只接受一个参数,即超时值。Sleep
会阻塞调用线程,直到指定的超时时间到期。如果将 INFINITE
传递给超时值,则只能通过外部终止线程来退出该线程。此函数的变体在计算的非常早期就已经存在,甚至可以在不支持多线程应用程序的系统中找到。
SleepEx
此函数结合了上面提到的 Sleep
函数和上面提到的许多“Ex”函数,例如 WaitForSingleObjectEx
。除了超时参数外,它还接受第二个布尔参数,该参数指示调用线程是否进入可警报等待状态。也就是说,函数会阻塞直到指定的超时时间到期,或者直到完成例程被排队。
WaitMessage
此函数会阻塞调用线程的执行,直到线程的消息队列中出现新消息。消息队列中已存在但线程尚未移除的消息(例如,使用 GetQueueStatus
或带有 PM_NOREMOVE
的 PeekMessage
等)将不会被视为新消息,在这种情况下,函数会继续阻塞线程。
WaitForInputIdle
此函数与其他上述函数略有不同,因为它只等待进程句柄(而不是线程句柄),而不等待任何其他同步对象。除了进程句柄之外,此函数还接受超时值作为参数。它会一直等待,直到超时到期,或者直到给定进程不再有待处理的输入消息。换句话说,直到进程等待用户输入。
Unix 操作系统支持进程之间的父子关系概念,而 Windows 没有。在 Unix 中,父进程可以使用 wait
或 waitpid
系统调用来等待子进程完成执行。使用 WaitForInputIdle
,可以模拟一个类似的概念,即一个进程可以等待它使用 CreateProcess
API 创建的另一个进程完成所有消息的处理。
结论
我希望本文能帮助您理解“等待函数”的基础知识以及这些函数是如何实际使用的。还有许多其他等待函数,如 WaitCommEvent
、WaitForDebugEvent
等,它们属于硬件、调试等范畴,并未在本文章中介绍。MAPI、DirectX 等许多其他库也包含更多等待函数。