WaitForMultipleObjects 的 C++ 包装器 第二部分 - 添加计时器支持






4.71/5 (8投票s)
描述了一个 C++ 类,该类封装了 WaitForMultipleObjects API 的使用模式,使其易于与 C++ 对象集成。
引言
在上一篇文章中,我讨论了一个围绕 WaitForMultipleObjects
API 的 C++ 封装器,它提供了一个通用的 API 接口,从而无需编写相当普遍的 switch-case 语句来处理其返回值并将控制权转移到适当的方法或函数。该封装器允许您动态添加和删除等待句柄,并将函数对象关联为等待句柄的处理函数。在本文中,我将为该类添加 Win32 计时器支持,以便类客户端可以注册函数对象,以便在预设时间间隔过后调用。
背景
大多数软件通常都有例行处理任务。此类任务的示例是执行未使用资源清理的内务管理功能,或监视网络应用程序中连接客户端健康状况的功能。用于实现此类机制的常见编程习惯用法是计时器,它涉及在预设时间间隔过后调用函数。计时器可以是完全自管理的,其中一个工作线程除了其他职责外,还定期检查是否应该调用所需的计时器任务。如果程序有 GUI,Windows 会在预设间隔将计时器消息发送到指定的窗口,在那里可以进行所需的处理。另一种替代方案是,如果底层操作系统提供必要的 API,则可以使用该 API 注册回调,这些回调将在特定时间间隔过后调用。Windows SDK 提供了一些实现最后一种计时器机制风格的函数。
Windows SDK 计时器 API 提供两种类型的计时器——手动重置计时器和同步计时器。前一种类型表现出与 Win32 事件非常相似的行为,因为它们可以在等待函数中等待被触发。当预设时间过去时,它们的状态变为已触发,并且监视该句柄的等待函数返回。后者使用异步过程调用 (APC) 来实现其功能,并要求管理计时器的线程处于 Microsoft 定义的“可警报等待状态”。当计时器间隔过去时,一个 APC 会排队到线程 APC 队列中,并在线程进入可警报等待状态时进行处理。
使用 Windows SDK 提供的计时器本质上效率更高,因为它们内置于调度程序中。使用它们还可以使代码更简单、更简洁,因为操作系统将负责跟踪已过去的时间间隔以及何时调用计时器函数(或触发计时器对象)。此外,可以创建和独立管理多个计时器对象,这有助于实现更好的封装。
为了将计时器功能集成到 WFMOHandler 中,我将使用手动重置计时器。我选择这种方式是因为它们在行为上更接近可等待对象,这使得实现类似于 WaitHandler<>
。此外,从 SetWaitableTimer 的 MSDN 文档来看,这种方法似乎比同步计时器提供更好的计时器传递保证。缺点是每个计时器都会占用有限的 WaitForMultipleObjects
句柄数组中的一个槽,从而减少可以监视的等待句柄的数量。
Using the Code
通过两个额外的方法——AddTimer()
和 RemoveTimer()
添加计时器支持。
AddTimer()
是一个函数模板,其签名与 AddWaitHandle()
非常相似。除了作为计时器处理程序的函数对象(当计时器过期时将调用)之外,它还采用两个额外的参数;计时器间隔(以毫秒为单位指定)和一个布尔值,用于指定这是一个一次性计时器还是重复计时器。顾名思义,一次性计时器就是这样——在预设间隔过后,计时器函数对象只调用一次,并且计时器不再可用并被销毁。另一方面,重复计时器的工作方式类似于旧的 GUI 窗口计时器,定期调用计时器函数对象,直到它被明确停止。该函数返回一个 unsigned
,它是刚刚创建的计时器的唯一 ID。此 ID 以后可以通过将其提供给 RemoveTimer()
方法来停止计时器。
要使用,请声明您的类并使其派生自 WFMOFHandler
,并通过提供计时器处理函数或方法作为函数对象来注册您需要的计时器。与等待句柄一样,此参数可以是显式构造的函数对象,也可以是使用 STL std::bind()
工具即时组合的函数对象。这两种用法都在下面的 MyDaemon
构造函数中显示。
#include "wfmohandler.h"
// A simple class that implements an asynchronous 'recv' UDP socket.
// Socket binds to loopback address!
class AsyncSocket {
USHORT m_port;
WSAEVENT m_event;
SOCKET m_socket;
// hide default and copy ctor
AsyncSocket();
AsyncSocket(const AsyncSocket&);
public:
AsyncSocket(USHORT port)
: m_port(port)
, m_event(::WSACreateEvent())
, m_socket(::WSASocket(AF_INET, SOCK_DGRAM, IPPROTO_UDP, NULL, 0, 0))
{
// bind the socket
struct sockaddr_in sin = {0};
sin.sin_family = AF_INET;
sin.sin_port = ::htons(port);
sin.sin_addr.s_addr = ::inet_addr("127.0.0.1");
if (m_event != NULL && m_socket != INVALID_SOCKET
&& ::bind(m_socket, reinterpret_cast<const sockaddr*>(&sin), sizeof(sin)) == 0) {
// put it in 'async' mode
if (::WSAEventSelect(m_socket, m_event, FD_READ) == 0)
return;
}
std::cerr << "Error initializing AsyncSocket, error code: " << WSAGetLastError() << std::endl;
// something went wrong, release resources and raise an exception
if (m_event != NULL) ::WSACloseEvent(m_event);
if (m_socket != INVALID_SOCKET) ::closesocket(m_socket);
throw std::exception("socket creation error");
}
~AsyncSocket()
{
::closesocket(m_socket);
::WSACloseEvent(m_event);
}
// for direct access to the embedded event handle
operator HANDLE() { return m_event; }
// Read all incoming packets in the socket's recv buffer. When all the packets
// in the buffer have been read, resets the associated Win32 event preparing it
// for subsequent signalling when a new packet is copied into the buffer.
void ReadIncomingPacket()
{
std::vector<char> buf(64*1024);
struct sockaddr_in from = {0};
int fromlen = sizeof(from);
int cbRecd = ::recvfrom(m_socket, &buf[0], buf.size(),
0, reinterpret_cast<sockaddr*>(&from), &fromlen);
if (cbRecd > 0) {
std::cerr << cbRecd << " bytes received on port " << m_port << std::endl;
} else {
int rc = ::WSAGetLastError();
if (rc == WSAEWOULDBLOCK) {
// no more data, reset the event so that WaitForMult..will block on it
::WSAResetEvent(m_event);
} else {
// something else went wrong
std::cerr << "Error receiving data from port " << m_port
<< ", error code: " << ::WSAGetLastError() << std::endl;
}
}
}
};
class MyDaemon : public WFMOHandler {
AsyncSocket m_socket1;
AsyncSocket m_socket2;
AsyncSocket m_socket3;
class AnotherEventHandler { // an explicit functor
AsyncSocket& m_socket;
public:
AnotherEventHandler(AsyncSocket& sock) : m_socket(sock) {}
void operator()() {
m_socket.ReadIncomingPacket(); // handle incoming socket data
}
};
AnotherEventHandler m_aeh;
public:
MyDaemon()
: WFMOHandler()
, m_socket1(5000)
, m_socket2(6000)
, m_socket3(7000)
, m_aeh(m_socket3)
{
// setup two handlers on the two AsyncSockets that we created
WFMOHandler::AddWaitHandle(m_socket1,
std::bind(&AsyncSocket::ReadIncomingPacket, &m_socket1));
WFMOHandler::AddWaitHandle(m_socket2,
std::bind(&AsyncSocket::ReadIncomingPacket, &m_socket2));
WFMOHandler::AddWaitHandle(m_socket3, m_aeh);
// install two timers -- a repeat timer and a one-off timer
// note how the timer ids are preserved for removal later (if necessary)
m_timerid = WFMOHandler::AddTimer(1000, true,
std::bind(&MyDaemon::RoutineTimer, this, &m_socket1));
m_oneofftimerid = WFMOHandler::AddTimer(3000, false,
std::bind(&MyDaemon::OneOffTimer, this));
}
virtual ~MyDaemon()
{
Stop();
// just being graceful, WFMOHandler dtor will cleanup anyways
WFMOHandler::RemoveWaitHandle(m_socket2);
WFMOHandler::RemoveWaitHandle(m_socket1);
}
void RoutineTimer(AsyncSocket* pSock)
{
pSock;
std::cout << "Routine timer has expired!" << std::endl;
}
void OneOffTimer()
{
std::cout << "One off tmer has expired!" << std::endl;
m_oneofftimerid = 0;
}
};
上面的代码与上一篇文章中使用的代码相同,只是添加了两个方法并将它们与新的计时器 API 连接起来。计时器 API 的两种用例——一次性计时器和重复计时器——都在示例中展示。
关注点
与等待句柄不同,计时器句柄由 WFMOHandler 内部管理。添加计时器时,将创建一个新的 Win32 计时器对象并将其添加到等待句柄数组中。此句柄对类客户端不可见,并在通过相应的 RemoveTimer()
方法调用删除计时器时释放。但是,对于一次性计时器,无论是否调用 RemoveTimer()
,计时器句柄都会在计时器函数返回后自动释放。
在底层,计时器接口是使用与等待句柄相同的技术实现的。然而,用于生成特定于计时器的类实例的内部类模板 TimerHandler<>
,在派生链中添加了一个额外的类 TimerIntermediate
。从表面上看,这个类似乎是为了保存计时器的唯一 ID 而创建的,该 ID 用于跟踪客户端类的删除请求,甚至可能显得多余,因为计时器 ID 可以存储在模板类 TimerHandler<>
中。然而,这个额外类存在于 TimerHandler<>
的派生链中还有另一个原因。
在派生链中拥有 TimerIntermediate
有助于我们将这些对象与来自公共 m_waithandlers
集合的 WaitHandler<>
类实例对象区分开来。这是必要的,因为在 RemoveTimer()
中我们需要找到与请求删除的计时器 ID 对应的 TimerHandler<>
类实例对象。然而,由于我们跟踪的所有等待句柄对应的对象都统一存储为指向基类 WaitHandlerBase
的指针,我们需要一种机制来区分这两个子类。这就是在派生链中拥有 TimerIntermediate
的用武之地。由于所有 TimerHandler<>
实例的派生链中也有一个 TimerIntermediate
,我们可以通过使用 dynamic_cast<TimerIntermediate*>
将 WaitHandlerBase
指针向上转换为 TimerIntermediate
来区分两者。
请注意,我们可以通过向该类添加一个额外的方法来区分 WaitHandlerBase
的两个子类,该方法默认返回布尔值。然后 TimerHandler<>
可以重写此方法并返回不同的值以识别自身。示例实现如下所示。
// base class for waitable triggers
struct WaitHandlerBase {
HANDLE m_h;
bool m_markfordeletion;
WaitHandlerBase(HANDLE h) : m_h(h), m_markfordeletion(false)
{}
virtual ~WaitHandlerBase()
{}
virtual bool IsTimer() { return false; }
virtual void invoke(WFMOHandler*) = 0;
};
template<typename Handler>
struct TimerHandler : public WaitHandlerBase, public TimerIntermediate {
...
virtual bool IsTimer() { return true; }
...
}
然而,这需要更改基类设计以适应派生类的要求。如果可能的话,理想情况下应避免这种情况。当然,这将是两种方法中更有效的方法,但由于在程序中删除计时器通常不是一项经常重复的任务,我认为这种设计(或者说是纯粹设计的妥协)是不必要的。
注意事项
实现的一个注意事项是,一旦创建了计时器,其持续时间就无法更改。如果要更改计时器间隔,唯一的解决方案是删除现有计时器,然后添加具有新间隔的新计时器。当然,这种行为可以改变,尽管这足以满足我开发这个类的需求。然而,实现这一点将需要扩展 TimerIntermediate
类,使其维护计时器间隔以及重复标志,以便可以从相关方法更新这些属性。
历史
2014 年 1 月 7 日 - 初次发布