`

借助 C++ 进行 Windows 开发---线程池取消和清理

 
阅读更多
取消和清理是相当困难的问题,以解决说到多线程应用程序。如果是,可以安全地关闭句柄?您是否需要考虑哪个线程取消的操作?更糟的是,一些多线程的 Api 不重入,潜在地提高性能,但还为开发人员增加的复杂性。

我引入了最后一个月的专栏中的线程池环境 (msdn.microsoft.com/magazine/hh394144)。此环境允许的一项关键功能是清理组,而这是什么我将重点此处。清理组不要尝试解决所有的世界取消和清理问题。他们所做的就是该线程池的对象和回调,使其更易于管理,并且这间接有助于简化取消和清除的其他 Api 和资源,根据需要。

到目前为止,我已经只向您显示如何使用 unique_handle 类模板自动关闭通过 CloseThreadpoolWork 函数的工作对象。(请参阅处的 8 月 2011年列 msdn.microsoft.com/magazine/hh335066 的详细信息。)但是有一些限制,使用此方法。如果要说中是否回调会取消挂起或不,您需要首先调用 WaitForThreadpoolWorkCallbacks。这使得两个函数调用的回调生成的 multipliednumber 对象正在使用您的应用程序。如果您倾向于使用 TrySubmitThreadpoolCallback,您甚至不能获得这样的机会,并会留下想知道如何取消或等待结果回调。当然,真实的应用程序很可能会远远不止是工作对象。在下个月的专栏中,我将从开始引入产生回调,从 i/o 的计时器,以便在可等待对象的其他线程池对象。协调取消和清理的所有这些将很快变成恶梦。幸运的是,清理组来解决这些问题以及更多内容。

CreateThreadpoolCleanupGroup 函数创建一个清理组对象。如果此函数成功,它将返回一个不透明的指针,该指针表示清理组对象。如果失败,会返回一个 null 指针值,并通过 GetLastError 函数提供更多信息。给定的清理组对象,CloseThreadpoolCleanupGroup 函数指示线程池可能会释放该对象。我曾提到过举措,但它应该再强调一下 — 线程池 API 不容许对其参数无效。调用 CloseThreadpoolCleanupGroup 或任何其他 API 函数使用以前关闭的无效或空指针值将导致应用程序崩溃。这些都是由程序员引入的缺陷,应该不需要额外的检查在运行时。我在我 2011 年 7 月的专栏中引入的 unique_handle 类模板 (msdn.microsoft.com/magazine/hh288076) 负责这些详细信息的帮助下清理组特定特性类的:

  1. struct cleanup_group_traits
  2. {
  3. static PTP_CLEANUP_GROUP invalid() throw()
  4. {
  5. return nullptr;
  6. }
  7. static void close(PTP_CLEANUP_GROUP value) throw()
  8. {
  9. CloseThreadpoolCleanupGroup(value);
  10. }
  11. };
  12. typedef unique_handle<PTP_CLEANUP_GROUP, cleanup_group_traits> cleanup_group;

我现在可以使用方便的 typedef 并创建一个清理组对象,如下所示:

  1. cleanup_group cg(CreateThreadpoolCleanupGroup());
  2. check_bool(cg);

如同专用池和回调优先级,清理组是与环境对象的方式的各种创造回调的对象相关联。首先,更新的环境,以指示将管理生存期的对象和自己的回调,像下面这样的清理组:

  1. environment e;
  2. SetThreadpoolCallbackCleanupGroup(e.get(), cg.get(), nullptr);

此时,您可以将对象添加到然后被称为清理组的成员的清理组中。这些对象可以还分别从删除的清理组中,但它是更常见关闭单个操作中的所有成员。

工作对象可能会变得清理组的成员在创建时只需通过提供给 CreateThreadpoolWork 函数更新的环境:

  1. auto w = CreateThreadpoolWork(work_callback, nullptr, e.get());
  2. check_bool(nullptr != w);

请注意我没有使用 unique_handle 这一次。新创建的工作对象目前环境的清理组的成员,并且不需要直接使用 RAII 跟踪其生存期。

可以仅通过关闭它,这可以个别地使用 CloseThreadpoolWork 函数来撤消工作对象的成员身份。线程池知道工作对象是清理组的成员,则在关闭前撤消其成员资格。这可以确保应用程序不会崩溃时清理组稍后尝试关闭其所有成员。逆说法不正确: 如果首先指示要关闭其所有成员的清理组,然后现在无效的工作对象上调用 CloseThreadpoolWork 时,您的应用程序将崩溃。

当然,整个清理组一点自由不必分别关闭所有使用发生的各种回调生成的对象的应用程序。更重要的是,它允许等待并有选择地取消任何未完成的回调中单个等待操作而不必有应用程序线程等待并重复恢复应用程序。CloseThreadpoolCleanupGroupMembers 函数提供了所有这些服务和详细信息:

  1. bool cancel = ...
  2. CloseThreadpoolCleanupGroupMembers(cg.get(), cancel, nullptr);

此函数可能看起来简单,但实际上它为在其所有成员的聚合执行多个重要职责。首先,具体取决于第二个参数的值,它将取消尚未尚未开始执行所有待执行回调。接下来,它等待已经开始执行任何回调和任何挂起的回调可选) 如果您选择不取消它们。最后,它将关闭所有的成员对象。

一些有 likened 清理组垃圾回收,但我认为这是一个令人误解的比喻。如果有的话) 清理组是更像是一个标准模板库 (STL) 容器回调生成的对象。添加到组中的对象将不自动关闭任何原因。如果您不能调用 CloseThreadpoolCleanupGroupMembers,您的应用程序将会泄漏内存。调用 CloseThreadpoolCleanupGroup 以关闭组本身不能帮助任何一个。您应该而只是认为清理组作为一种管理生命周期和并发的一组对象。当然,可以单独管理不同的组对象的应用程序中创建多个清理组。它是一种极其有用的抽象 — — 但它不是幻数,并必须小心才能正确使用。请考虑下面的故障伪代码:

  1. environment e;
  2. SetThreadpoolCallbackCleanupGroup(e.get(), cg.get(), nullptr);
  3. while (app is running)
  4. {
  5. SubmitThreadpoolWork(CreateThreadpoolWork(work_callback, nullptr, e.get()));
  6. // Rest of application.
  7. }
  8. CloseThreadpoolCleanupGroupMembers(cg.get(), true, nullptr);

可预知,此代码将使用非常的大量的内存,并将获得较慢和慢系统资源将会耗尽。

在我年 8 月 2011年列,作者说明了看似简单的 TrySubmitThreadpoolCallback 函数是相当有问题,因为没有要等待的时间来完成其回调的简单方法。这是因为工作对象实际上不公开的 API。线程池本身,但是,会受到没有此类限制。由于 TrySubmitThreadpoolCallback 接受一个指针,指向一个环境,因此可以间接地使对象清理组的成员的生成工作。这种方式,您可以使用 CloseThreadpoolCleanupGroupMembers 等待或取消生成回调。请考虑下面的改进伪代码:

  1. environment e;
  2. SetThreadpoolCallbackCleanupGroup(e.get(), cg.get(), nullptr);
  3. while (app is running)
  4. {
  5. TrySubmitThreadpoolCallback(simple_callback, nullptr, e.get());
  6. // Rest of application.
  7. }
  8. CloseThreadpoolCleanupGroupMembers(cg.get(), true, nullptr);

由于线程池自动关闭工作对象创建的 TrySubmitThreadpoolCallback,我无法几乎 forgive 多谢这与垃圾回收,开发人员。当然,这有与清理组无关。我在我 2011 年 7 月的专栏中介绍了这种行为。CloseThreadpoolCleanupGroupMembers 函数在这种情况下不是最初负责关闭的工作对象,但只能等待和可能取消回调。与前面的示例中,这一将无限期地运行,而不使用频频面临不必要的资源,而仍提供 
predictable 取消和清理。回调组的帮助下,TrySubmitThreadpoolCallback 兑现本身,提供一个安全而便利的替代方案。在反复排队相同的回调是高度结构化的应用程序,它仍然是更有效地重复使用显式工作对象,但可不能再将其忽略此函数的便利。

清理组提供一个最终的功能来简化您的应用程序清理要求。通常情况下,它是不够只需等待完成未完成的回调。您可能需要为回调生成的每个对象执行一些清理任务,一旦您确定回调将不会再执行。具有管理这些对象的生存期清理组还意味着线程池处于知道此类清理任务时应该发生的最佳位置。

当您将清理组与 SetThreadpoolCallbackCleanupGroup 函数通过环境相关联时,您还可以提供清理组的每个成员作为关闭这些对象的 CloseThreadpoolCleanupGroupMembers 函数的过程的一部分执行的回调。由于这是环境的一个属性,甚至可以属于同一个清理组的不同对象应用不同的回调。在下面的示例创建一个清理组和清理回调的环境:

  1. void CALLBACK cleanup_callback(void * context, void * cleanup)
  2. {
  3. printf("cleanup_callback: context=%s cleanup=%s\n", context, cleanup);
  4. }
  5. environment e;
  6. SetThreadpoolCallbackCleanupGroup(e.get(), cg.get(), cleanup_callback);

清理回调的第一个参数是回调生成对象的上下文值。例如,这是调用 CreateThreadpoolWork 或 TrySubmitThreadpoolCallback 函数时指定的上下文值,它是您如何知道哪一个对象正在清理回调调用的。清理回调的第二个参数是调用 CloseThreadpoolCleanupGroupMembers 函数时,作为最后一个参数提供的值。

现在考虑一下以下工作对象和回调:

  1. void CALLBACK work_callback(PTP_CALLBACK_INSTANCE, void * context, PTP_WORK)
  2. {
  3. printf("work_callback: context=%s\n", context);
  4. }
  5. void CALLBACK simple_callback(PTP_CALLBACK_INSTANCE, void * context)
  6. {
  7. printf("simple_callback: context=%s\n", context);
  8. }
  9. SubmitThreadpoolWork(CreateThreadpoolWork(work_callback, "Cheetah", e.get()));
  10. SubmitThreadpoolWork(CreateThreadpoolWork(work_callback, "Leopard", e.get()));
  11. check_bool(TrySubmitThreadpoolCallback(simple_callback, "Meerkat", e.get()));

以下哪种不是像其他人?刻意少 Meerkat 希望能够像他相邻的大猫在南部非洲,不如说他只是永远不会将其中之一。清理组成员都将关闭,如下所示时会发生什么?

  1. CloseThreadpoolCleanupGroupMembers(cg.get(), true, "Cleanup");

很小是某些多线程代码中。如果它们被取消并关闭前执行管理回调,以下可能打印:


          work_callback: context=Cheetah
work_callback: context=Leopard
simple_callback: context=Meerkat
cleanup_callback: context=Cheetah cleanup=Cleanup
cleanup_callback: context=Leopard cleanup=Cleanup
        

一个常见错误是假定清理回调称为仅对其回调未获得机会执行的对象。Windows API 是有点令人误解,因为它有时是指清理回调作为取消回调,但这不是这种情况。为每个当前组成员的清理只需调用清理回调。您可以将其视为析构函数用于清理组成员,但是,如同的垃圾回收隐喻,这并不是没有风险。此比喻拦截精美也直到再次引入了复杂的 TrySubmitThreadpoolCallback 函数。请记住线程池自动关闭此函数创建只要其回调执行基础工作对象。这意味着该隐式工作对象的执行清理回调依赖其回调已经开始通过调用 CloseThreadpoolCleanupGroupMembers 的时间来执行。仅清理回调将执行该隐式工作对象中,如果其工作回调为挂起的静止并且询问 CloseThreadpoolCleanupGroupMembers 取消任何挂起的回调。这是所有而是不可预知,并且因此不建议使用带有清理回调的 TrySubmitThreadpoolCallback。

最后,值得一提,即使 CloseThreadpoolCleanupGroupMembers 块,它不会浪费任何时间。已做好清理的任何对象将具有等待其他未完成的回调完成时调用的线程上执行其清理回调。清理组所提供的功能 — — 并且,特别是 CloseThreadpoolCleanupGroupMembers 函数 — 是无价的高效、 干净地关闭您的应用程序的整体或局部撕裂。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics