Windows 使用 c + +

线程池计时器和 I/O

Kenny Kerr

Kenny Kerr
在这方面,我最后安装 Windows 7 的线程池的我要去支付余下创收回调对象由 API 提供的两次。有更多的是我可以写的线程池的但是后五个文章,涵盖几乎所有其功能,您应该有效和有效率地打开您的应用程序使用起来得心应手。

我 8 月 (msdn.microsoft.com/magazine/hh335066) 和 11 月 (msdn.microsoft.com/magazine/hh547107) 我所描述的列,工作和等待对象分别。工作对象允许您提交工作,一个函数,直接执行的线程池的形式。该函数将尽早执行。等待对象告诉您的名义,等待内核同步对象和队列功能,当它发出信号的线程池。这是一个可扩展替代传统的同步基元和有效的替代方法来投票。不过,有很多情况下,计时器在哪里需要一定的时间间隔之后或在一些定期执行某些代码。这可能是因为缺乏一些 Web 协议或者"推送"支持的因为您在实施式 UDP 通信协议,您需要处理重新传输。幸运的是,线程池 API 提供了一个计时器对象,以高效和现在熟悉的方式处理所有这些情形。

计时器对象

CreateThreadpoolTimer 函数创建一个计时器对象。如果函数成功,则返回一个不透明的指针,该指针表示计时器对象。如果失败,它将返回一个空指针值,并提供通过 GetLastError 函数的详细信息。CloseThreadpoolTimer 函数给定的计时器对象,通知线程池,该对象可能会发布。如果你一直走系列中,这应该所有的发音很熟悉。这里是我介绍我的 2011 年 7 月列中的方便 unique_handle 类模板,可以用的性状类 (msdn.microsoft.com/magazine/hh288076):

struct timer_traits
{
  static PTP_TIMER invalid() throw()
  {
    return nullptr;
  }
  static void close(PTP_TIMER value) throw()
  {
    CloseThreadpoolTimer(value);
  }
};
typedef unique_handle<PTP_TIMER, timer_traits> timer;

我现在可以使用 typedef,并创建一个计时器对象,如下所示:

void * context = ...
timer t(CreateThreadpoolTimer(its_time, context, nullptr));
check_bool(t);

和往常一样,最后一个参数 (可选) 接受一个指针,指向一个环境可以将计时器对象关联的环境,所以在我 2011 年 9 月列述 (msdn.microsoft.com/­杂志/hh416747)。 第一个参数是将排队等待,每次在计时器过期的线程池的回调函数。 计时器回调被声明,如下所示:

void CALLBACK its_time(PTP_CALLBACK_INSTANCE, void * context, PTP_TIMER);

若要控制在计时器过期的时间和频率,使用 SetThreadpoolTimer 函数。 当然,其第一个参数提供计时器对象,但第二个参数表示的计时器的过期的到期时间。 它使用一个 gmt 时结构来描述绝对的或相对的时间。 如果你不能肯定这是如何工作的我鼓励您阅读上月的列中,在我所描述的语义 gmt 时结构的详细信息。 下面是一个简单的例子,我在其中设置要在 5 秒钟内过期的计时器:

union FILETIME64
{
  INT64 quad;
  FILETIME ft;
};
FILETIME relative_time(DWORD milliseconds)
{
  FILETIME64 ft = { -static_cast<INT64>(milliseconds) * 10000 };
  return ft.ft;
}
auto due_time = relative_time(5 * 1000);
SetThreadpoolTimer(t.get(), &due_time, 0, 0);

同样,如果你不确定有关的 relative_time 功能是如何工作的请阅读我 2011 年 11 月的专栏文章。 在此示例中,计时器期满后五秒,此时的线程池会排队 its_time 回调函数的实例。 除非采取行动,将排队没有进一步回调。

SetThreadpoolTimer 还可用于创建定期将一些固定的间隔中排队回调的计时器。 例如:

auto due_time = relative_time(5 * 1000);
SetThreadpoolTimer(t.get(), &due_time, 500, 0);

在此示例中,计时器的回调是首先排队后五秒钟,然后每半秒后,直到重置计时器对象或关闭。 不同的到期时间,只是以毫秒为单位指定期间。 请记住,定期的计时器将排队等待回调后经过一段时间的无论多长时间来执行回调。 这意味着它有可能要同时运行多个回调或重叠,如果间隔为足够小或回调采取足够长的时间来执行。

如果您需要确保回调不重叠,并不是重要的是,然后不同的方法,用于创建一个定期的计时器可能适当的每个期间的精确的开始时间。 SetThreadpoolTimer 调用中指定的期间,而不是简单地重置计时器回调本身中。 这种方式,您可以确保回调将永远不会重叠。 如果没有别的办法,这简化了调试。 想象一下逐句通过计时器回调,却发现线程池已排队几的更多实例,虽然你是分析您的代码 (或加气你的咖啡) 在调试器中。 使用此方法时,这将永远不会发生了。 下面是这个样子:

void CALLBACK its_time(PTP_CALLBACK_INSTANCE, void *, PTP_TIMER timer)
{
  // Your code goes here
  auto due_time = relative_time(500);
  SetThreadpoolTimer(timer, &due_time, 0, 0);
}
auto due_time = relative_time(5 * 1000);
SetThreadpoolTimer(t.get(), &due_time, 0, 0);

你可以看到,初始的截止时间是 5 秒,然后我为 500 毫秒的回调结束时重置的到期时间。 我已经利用这一事实的回调签名提供一个指向原始的计时器对象,把重置计时器的工作很简单。 你也可能想要确保对 SetThreadpoolTimer 的调用可靠地调用回调函数返回之前使用 RAII。

到期时间停止任何未来的计时器过期时间,可能会导致进一步回调的空指针值,您可以调用 SetThreadpoolTimer。 您还需要调用 WaitForThreadpool­TimerCallbacks 为避免争用状态。 当然,计时器对象同样工作井清理组,用我 2011 年 10 月的专栏文章中所述。

因为文档是指"窗口长度",以及延迟,SetThreadpoolTimer 的最后一个参数可以是有点混乱。 所有有关的那是什么? 这是实际功能,会影响能源效率,并有助于减少总体电力消费。 它基于一种叫做合并的计时器。 显然,最好的解决办法是完全避免计时器和使用事件来代替。 这允许系统的处理器最多的空闲时间,从而鼓励他们进入他们尽可能多的低功耗空闲状态。 不过,如果计时器是必要的合并的计时器可以降低整体功耗通过减少所需的计时器中断的数量。 合并的计时器基于计时器过期"可以容忍拖延"的想法。 鉴于一些可以容忍拖延,Windows 内核可调整的实际的过期时间,以配合现有的任何计时器。 良好经验法则是将延迟设置为使用期间的十分之一。 例如,如果计时器的过期 10 秒后,使用一秒钟的延迟,取决于什么是适合您的应用程序。 延迟越大,更多的机会,内核必须优化其计时器中断。 另一方面,任何小于 50 ms 不会多大用处的因为它开始染指内核的默认时钟间隔。

I/O 完成对象

现在是时候为我介绍的线程池 API 的创业板: 输入/输出 (I/O) 完成对象或简单的 I/O 的对象。 当年我第一次引入线程池 API,所述线程池上生成的 I/O 完成端口 API。 传统上,在 Windows 上实施的最具可扩展性的 I/O 是可能只使用 I/O 完成端口 API。 我过去写关于此 API。 虽然并不特别难使用,它并不总是需要易于集成和应用程序的其他线程。 多亏了线程池 API,不过,你也与单个 API 的工作、 同步、 计时器和现在 I/O,这两个领域的最佳产品。 其他的好处是执行重叠的 I/O 完成线程池是比使用 I/O 完成端口 API,尤其是当它来同时处理多个文件句柄和多个重叠的操作其实更直观。

正如您可能已经猜到了,CreateThreadpoolIo 函数创建一个对象,I/O 和 CloseThreadpoolIo 函数通知线程池,该对象可能会发布。 这里是一个 unique_handle 类模板的性状类:

struct io_traits
{
  static PTP_IO invalid() throw()
  {
    return nullptr;
  }
  static void close(PTP_IO value) throw()
  {
    CloseThreadpoolIo(value);
  }
};
typedef unique_handle<PTP_IO, io_traits> io;

CreateThreadpoolIo 函数接受文件句柄,这意味着 I/O 对象都能够控制单个对象的 I/O。 当然对象需要支持重叠的 I/O,但这包括常见资源类型如文件系统的文件命名管道,套接字,等等。 让我用一个简单的例子,等待接收使用套接字的 UDP 数据包的演示。 若要管理插座,我将使用 unique_handle 与下面的性状类:

struct socket_traits
{
  static SOCKET invalid() throw()
  {
    return INVALID_SOCKET;
  }
  static void close(SOCKET value) throw()
  {
    closesocket(value);
  }
};
typedef unique_handle<SOCKET, socket_traits> socket;

在这种情况下与不同特性类,它展示了到目前为止,无效的函数不返回 null 指针值。 这是因为 WSASocket 函数,如同 CreateFile 函数,使用非同寻常的价值来表示无效句柄。 鉴于此类性状和 typedef,我可以创建套接字和 I/O 对象很简单:

socket s(WSASocket( ...
, WSA_FLAG_OVERLAPPED));
check_bool(s);
void * context = ...
io i(CreateThreadpoolIo(reinterpret_cast<HANDLE>(s.get()), io_completion, context, nullptr));
check_bool(i);

信号的任何 I/O 操作完成的回调函数声明,如下所示:

void CALLBACK io_completion(PTP_CALLBACK_INSTANCE, void * context, void * overlapped,
  ULONG result, ULONG_PTR bytes_copied, PTP_IO)

如果您使用过重叠的 I/O 之前,此回调函数的唯一参数应熟悉。 因为重叠的 I/O 是异步的性质,并允许重叠的 I/O 操作 — — 因此名称重叠我 / O — — 那里需要有一种方法来标识特定的 I/O 操作已完成。 这是参数的重叠的目的。 此参数提供的重叠或 WSAOVERLAPPED 的结构是一个指向指定当第一次启动特定的 I/O 操作。 传统的方法包装成大挂掉此参数的更多数据结构的重叠结构仍可使用。 重叠的参数提供了用于标识特定的 I/O 操作已完成,同时该上下文参数的方法 — — 像往常一样 — — 提供的 I/O 端点,无论任何特定操作的上下文。 鉴于这两个参数,您应该毫无困难地协调通过您的应用程序数据的流动。 结果参数将告诉您是否重叠的操作成功与平常的 ERROR_SUCCESS 或为零,表示成功。 最后,bytes_copied 参数显然告诉你多少字节是实际读取或写入。 一个常见的错误是假定实际上已复制请求的字节数。 Don’t make that mistake: it’s the very reason for this parameter’s existence.

唯一的是有一个稍微复杂的线程池中的 I/O 支持部分是本身的 I/O 请求的处理。 它负责这正确的代码。 在调用函数以初始化一些异步 I/O 操作,例如,读取或 WSARecvFrom 之前, 必须调用要让知道 I/O 操作即将开始的线程池的 StartThreadpoolIo 函数。 诀窍是,如果碰巧完成同步,然后通过调用 CancelThreadpoolIo 函数必须通知此线程池的 I/O 操作。 请记住,I/O 完成并不一定等于成功完成。 I/O 操作可能成功或失败都同步或异步。 不管怎样,如果 I/O 操作将不会通知完成端口的完成,您需要让知道的线程池。 下面是这可能类似接收 UDP 数据包的上下文中:

StartThreadpoolIo(i.get());
auto result = WSARecvFrom(s.get(), ...
if (!result)
{
  result = WSA_IO_PENDING;
}
else
{
  result = WSAGetLastError();
}
if (WSA_IO_PENDING != result)
{
  CancelThreadpoolIo(i.get());
}

正如您所看到的我开始通过调用 StartThreadpoolIo 告诉线程池中的 I/O 操作即将开始的过程。 我然后调用 WSARecvFrom 获得的东西。 解释结果是重要的部分。 WSARecvFrom 函数返回零,如果该操作成功完成,但完成端口将仍会收到通知,所以我改变结果为 WSA_IO_PENDING。 WSARecvFrom 从任何其他结果指示故障,与异常,当然,WSA_IO_PENDING 本身,这只是意味着操作已成功启动,但它将会在稍后完成。 现在,我只需调用 CancelThreadpoolIo 如果结果不是保持线程池加速挂起。 不同的 I/O 端点可以提供不同的语义。 例如,可以配置文件 I/O 以避免通知同步完成后的完成端口。 然后,需要适当地调用 CancelThreadpoolIo。

像回调生成中的其他对象的线程池 API,等待 i/o 回调对象可以取消使用 WaitForThreadpoolIoCallbacks 函数。 不过请记住这将取消任何挂起的回调,但不是取消任何挂起的 I/O 操作本身。 您需要使用适当的函数来取消操作以避免任何争用条件。 这样,您就可以安全地无任何重叠结构,以及等等。

这是它的线程池 API。 正如我所说的还有更多我可以写此功能强大的 API,但鉴于详细的演练提供了到目前为止,我相信你是你路上电源下一个应用程序中使用它。 加入我下个月,随着我继续探索与 c + + 的 Windows。

Kenny Kerr 是软件匠师是充满热情的本机 Windows 发展。他在到达 kennykerr.ca