借助 C++ 进行 Windows 开发

线程池同步

Kenny Kerr

 

Kenny Kerr
我已经说过: 阻止操作是并发的坏消息。通常,但是,您需要等待一些资源变得可用,或者可能您在实施一种协议,规定: 需要重新发送网络数据包之前经过一段时间。您做什么呢?您无法使用关键节、 调用函数 (如睡眠和 WaitForSingleObject,依此类推。当然,这意味着您必须只需坐周围再次阻塞的线程。您需要的是一种方法使您的名义而不会影响并发性极限状态,我在我 2011 年 9 月的专栏中介绍的等待线程池 (msdn.microsoft.com/magazine/hh394144)。资源是可用,或在该时间后,线程池可以再排队回调。

在本月的专栏中,我将向您展示如何实现这一。一起工作对象,我在我 2011 年 8 月的专栏中引入 (msdn.microsoft.com/magazine/hh335066),线程池 API 提供了许多其他回调生成的对象。本月,我将向您展示如何使用等待对象。

等待对象

线程池的等待对象用于同步。而非临界区上块 — 或轻盈读取器/写入器锁 — 您可以等待内核同步对象,通常一个事件或信号量,为成为终止。

尽管您可以使用 WaitForSingleObject 和朋友、 等待对象将与其余的线程池 API 可以很好地集成。这一点非常有效地通过提交,减少所需的线程数和您需要编写和调试的代码量任何等待对象组合在一起。这使您可以使用线程池环境和清理组,还使您不必使用专门等待为成为终止的对象的一个或多个线程。由于要在线程池的内核部分中的改进,它可以在某些情况下甚至实现这个目的无线程的方式。

CreateThreadpoolWait 函数创建一个等待对象。如果此函数成功,则返回表示等待对象的不透明指针。如果失败,会返回一个 null 指针值,并通过 GetLastError 函数提供更多信息。给定的工作对象,CloseThreadpoolWait 函数通知线程池对象可能会释放。此函数不返回值,并且对于效率假定等待对象有效。

我在我 2011 年 7 月的专栏中引入的 unique_handle 类模板 (msdn.microsoft.com/magazine/hh288076) 负责处理这些详细信息。

下面是 unique_handle 可以使用的一个特征类以及为方便起见使用的一个 typedef:

struct wait_traits
{
  static PTP_WAIT invalid() throw()
  {
    return nullptr;
  }
 
  static void close(PTP_WAIT value) throw()
  {
    CloseThreadpoolWait(value);
  }
};
 
typedef unique_handle<PTP_WAIT, wait_traits> wait;

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

void * context = ...
wait w(CreateThreadpoolWait(callback, context, nullptr));
check_bool(w);

像平常一样,最后一个参数 (可选) 接受指针到环境中,以便您可以将等待对象关联的环境中,如我在我 9 月的专栏中所述。 第一个参数是将排队等待的线程池等待完成后,该回调函数。 等待回调声明,如下所示:

void CALLBACK callback(PTP_CALLBACK_INSTANCE, void * context, PTP_WAIT, TP_WAIT_RESULT);

回调 TP_WAIT_RESULT 参数是只是一个无符号的整数提供为什么等待完成的原因。 WAIT_OBJECT_0 的值表示如发出信号同步对象成为已满足的等待时间。 另外,WAIT_TIMEOUT 的值表示的超时间隔已用之前同步对象处于终止状态。 您如何指示等待超时和同步对象? 这是非常复杂的 SetThreadpoolWait 函数的作业。 此函数非常简洁,直到您尝试指定超时。 请看以下示例:

handle e(CreateEvent( ...
));
check_bool(e);
 
SetThreadpoolWait(w.get(), e.get(), nullptr);

首先,创建一个事件对象,使用 unique_handle typedef 从我 7 月列。 毫不奇怪,SetThreadpoolWait 函数设置等待对象是等待的同步对象。 最后一个参数表示可选的超时,但在此示例中,我提供 null 指针的值,该值指示线程池中应无限期等待。

FILETIME 结构

但特定超时呢? 这就是获取棘手。 像 WaitForSingleObject 这样的函数,您可以以毫秒为单位的超时值设置为无符号整数。 SetThreadpoolWait 函数,但是,需要 FILETIME 结构,开发人员将面临几个难题的指针。 FILETIME 结构是一个 64 位值,表示绝对日期和自 1601年年年初以 100 纳秒为间隔 (基于协调通用时间) 以来的时间。

为了适应相对的时间间隔,SetThreadpoolWait 的 FILETIME 结构视为一个有符号的 64 位值。 如果提供一个负值,则它将无符号的值作为相对于当前时间,时间间隔再次以 100 纳秒为间隔。 值得一提的相对计时器停止计数时计算机是睡眠或休眠状态。 通过这显然不受影响绝对的超时值。 总之,此 FILETIME 不是使用方便的任一绝对或相对的超时值。

可能的绝对超时值最简单的方法是填写系统结构,然后使用 SystemTimeToFileTime 函数来为您准备的 FILETIME 结构:

SYSTEMTIME st = {};
st.wYear = ...
st.wMonth = ...
st.wDay = ...
st.wHour = ...
// etc.
FILETIME ft;
check_bool(SystemTimeToFileTime(&st, &ft));
 
SetThreadpoolWait(w.get(), e.get(), &ft);

对于相对的超时值,会涉及更多的思考。 首先,您需要将一些相对时间转换成 100 纳秒为时间间隔,然后将其转换为 64 位值为负。 后者是比似乎棘手。 请记住计算机表示有符号的整数,2 的补数系统,使用一个负值必须设置高其最高有效位的效果。 添加到这是这一事实 FILETIME 实际上包含两个 32 位值。 这意味着,您还需要正确地处理机器对齐方式时将其当作一个 64 位值,否则对齐方式可能会出现故障。 此外,您不能只需使用较低的 32 位值存储值,较高的 32 位值中的最高有效位原样。

相对超时值的转换

很普遍表达相对超时 (以毫秒为单位,因此让我演示该转换在此处。 请记住一毫秒是第二个千分位,一纳秒等于十亿分之一秒第二个。 看一看它的另一种方法是毫秒是 1000 毫秒为单位),和微秒是 1000 毫微秒为单位。 毫秒然后是度量的 10000 个 100 纳秒为单位,SetThreadpoolWait 所需单位。 有许多方法来表示这一点,但这里工作的一种方法:

DWORD milliseconds = ...
auto ft64 = -static_cast<INT64>(milliseconds) * 10000;
 
FILETIME ft;
memcpy(&ft, &ft64, sizeof(INT64));
 
SetThreadpoolWait(w.get(), e.get(), &ft);

请注意我是仔细强制转换之前乘法的 dword 值,以避免出现整数溢出。 我还可以使用 memcpy,因为 reinterpret_cast 需要一个 8 字节边界上对齐 FILETIME。 可以,当然,这相反,做,但这是一位更清晰。 更简单的方法充分利用了这一事实,Visual c + + 编译器将对齐具有最大的对齐要求,任何其成员的联合。 事实上,如果您订购了正确的联合的成员可以执行它只是一个行中,,如下所示:

union FILETIME64
{
  INT64 quad;
  FILETIME ft;
};
 
FILETIME64 ft = { -static_cast<INT64>(milliseconds) * 10000 };
 
SetThreadpoolWait(w.get(), e.get(), &ft.ft);

足够编译器技巧。 让我们回到线程池。 您可能会尝试另一件事是零超时。 这种情况通常使用 WaitForSingleObject 作为一种方式来确定是否没有实际上阻止和等待信号同步对象。 但是,此过程不支持由线程池,以便您最好仍坚持使用 WaitForSingleObject。

如果要停止等待其同步对象的特定工作对象,然后只需调用 SetThreadpoolWait 空指针值作为其第二个参数。 只需注意明显争用条件。

等待对象相关的最终函数是 WaitForThreadpoolWaitCallbacks。 首先,它可能出现类似于使用与工作对象,我在我 8 月的专栏中引入的 WaitForThreadpoolWorkCallbacks 函数。 不要让它愚弄您。 WaitForThreadpoolWaitCallbacks 函数字面上没有其名称的建议。 它会等待任何回调从特定等待对象。

问题是当等待对象相关联的同步对象处于终止状态或在超时过期时,将只排队回调。 发生这些事件之一,直到没有回调进行排队,没有任何要等待的等待函数。 解决方法是首先使用空指针值,指出要停止等待、,然后调用 WaitForThreadpoolWaitCallbacks,以避免任何争用条件的等待对象调用 SetThreadpoolWait:

SetThreadpoolWait(w.get(), nullptr, nullptr);
WaitForThreadpoolWaitCallbacks(w.get(), TRUE);

如您所料,第二个参数确定是否任何挂起的可能通过非检测到但尚未开始执行的回调将被取消或不。 自然,等待对象很好地与清理组的工作。 您可以阅读我 10 月 2011年 (msdn.microsoft.com/magazine/hh456398),了解如何使用清理组的列。 在大型项目中,它们确实不帮助简化大量棘手的取消和需要完成的清理。

 

Kenny Kerr 是一位热衷于本机 Windows 开发的软件专家。您可以通过 kennykerr.ca 与他联系。

这要归功于以下技术专家审阅这篇文章: Pedro Teixeira