【问题标题】:Suspend and Resume thread (Windows, C)挂起和恢复线程(Windows,C)
【发布时间】:2011-07-09 23:25:21
【问题描述】:

我目前正在开发一个高度多线程的应用程序,处理大量要处理的小数据批处理。

它的问题是产生了太多线程,这会大大降低系统速度。为了避免这种情况,我有一个句柄表,它限制了并发线程的数量。然后我“WaitForMultipleObjects”,当一个槽被释放时,我创建一个新线程,用它自己的数据批处理来处理。

现在,我拥有任意数量的线程(通常每个内核一个)。即便如此,多线程带来的负载也是非常合理的。这样做的原因:数据批量很小,所以我不断地创建新线程。

我目前正在实施的第一个想法是将作业重新组合成更长的序列列表。因此,当我创建一个新线程时,它会在被终止之前处理 128 或 512 个数据批处理。它运行良好,但在某种程度上破坏了粒度。

我被要求寻找另一种情况:如果问题来自过于频繁地“创建”线程,那么“暂停”它们、加载数据批处理并“恢复”线程呢?

不幸的是,我不太成功。 问题是:当线程处于“挂起”模式时,“WaitForMultipleObjects”不会检测到它是可用的。事实上,我无法有效地区分活动线程和挂起线程。

所以我有两个问题:

  1. 如何检测“挂起的线程”,以便我可以将新数据加载到其中并恢复它?

  2. 这是个好主意吗?毕竟,“CreateThread”真的是资源猪吗?

编辑

经过大量测试,这是我关于线程池和 IO 完成端口的发现,这两个都在这篇文章中提出。

使用旧版本“QueueUserWorkItem”测试线程池。 IO Completion Port 需要使用 CreateIoCompletionPort、GetQueuedCompletionStatus 和 PostQueuedCompletionStatus;

1) 首先是性能:创建许多线程的成本非常高,线程池和 io 完成端口都在避免这种成本方面做得很好。我现在每批 8 个作业,从之前的每批 512 个作业,没有减速。这是相当可观的。即使每批执行 1 个作业,性能影响也小于 5%。真是了不起。

从性能的角度来看,QueueUserWorkItem 胜出,尽管差距很小(大约高出 1%),几乎可以忽略不计。

2) 关于使用简单性: 关于启动线程:毫无疑问,QueueUserWorkItem 是迄今为止最容易设置的。相比之下,IO Completion 端口是重量级的。 关于结束线程:赢得 IO 完成端口。 由于某些未知的原因,MS 在 C 中没有提供函数来知道所有作业何时使用 QueueUserWorkItem 完成。它需要一些讨厌的技巧才能成功实现这个基本但关键的功能。这种缺乏功能没有任何借口。

3) 关于资源控制:IO Completion Port Big win,允许微调并发线程数,而 QueueUserWorkItem 没有这样的控制,它将愉快地花费所有 CPU 周期从所有可用的核心。这本身可能会破坏 QueueUserWorkItem。 请注意,较新版本的完成端口似乎允许该控制,但仅适用于 Windows Vista 及更高版本。

4) 关于兼容性:IO 完成端口的小胜利,从 Windows NT4 开始可用。 QueueUserWorkItem 仅存在于 Windows 2000 之后。但这已经足够了。更新版本的完成端口不适用于 Windows XP。

可以猜到,我在这两种解决方案之间非常纠结。他们都正确回答了我的需求。 对于一般情况,我建议 I/O Completion Port,主要用于资源控制。 另一方面,QueueUserWorkItem 更容易设置。很遗憾,它在要求程序员单独处理作业结束检测时失去了大部分这种简单性。

【问题讨论】:

    标签: c windows multithreading


    【解决方案1】:

    如果您还想支持 Windows XP,则不能使用 CreateThreadpool -- 否则,如果 Vista 和更新版本就足够了,Windows 线程池是最简单的方法。

    如果需要 Windows XP 支持,请生成多个线程并将它们分配给 IO completion port,然后让每个线程块在 GetQueuedCompletionStatus() 上。完成端口允许您将事件发布到每个事件将唤醒一个线程的端口,并且它们非常有效。他们在唤醒线程时也使用 LIFO 策略来保持缓存温暖。

    无论如何,您将永远不会想要暂停线程。永远不能。阻止、等待,但不要暂停。

    原因是挂起您会遇到您描述的问题,而且您会产生死锁,例如如果您的线程在关键部分或互斥锁内。除了调试器,没有人需要暂停线程。

    【讨论】:

    • 非常好。是的,Windows XP 支持是必须的(虽然我主要在 Win7 下开发,所以有时我必须在 XP 下做一些测试以确保兼容性)。几个月前我听说过 IO Completion 端口。我只是明白它很好而且很快,但是,这还不足以正确获得它;我再看看它。谢谢。
    • 完成端口起初看起来很吓人,但它们非常简单。创建一个将 NULL 作为“现有”参数的函数,它为您提供了完成端口的句柄。将其传递给您创建的所有线程。然后,在每个线程中,再次创建一个完成端口,但这一次将句柄作为“现有”参数传入。就是这样,现在您可以使用主线程中的 PostQueuedCompletionStatus 和工作线程中的 GetQueuedCompletionStatus。 MSDN 上的文档非常完整,绝对值得一读。
    • 感谢您的详细解释。这绝对值得一试。
    • 对于任何阅读本文的人来说,上述建议是不准确的。在某些情况下,暂停线程是非常有益的(尽管在 OP 的情况下,线程池确实更好)。甚至还有一个称为 Fiber 的通用对象就是为此而构建的。光纤也一直支持到 Windows XP:docs.microsoft.com/en-us/windows/win32/procthread/fibers
    【解决方案2】:

    是的,CreateThread 涉及相当多的开销。一种解决方案是使用线程池QueueUserWorkItem。另一种方法是启动一组线程并让它们从线程安全队列中检索“作业项”。

    【讨论】:

    • 确实如此。创建自己的事件,完成后将其设置在工作人员中。
    • 使用 CreateEvent() 和 SetEvent()。
    • 别生气。 WaitForMultipleObjects() 是我目前使用的函数,如果我知道它与 QueueUserWorkItem 兼容,我会很乐意使用它。问题是,这个函数需要获取一个句柄数组作为参数。但是使用 QueueUserWorkItem 时的句柄数组在哪里? MS 的在线文档在此功能上非常稀缺(msdn.microsoft.com/en-us/library/ms684957%28v=vs.85%29.aspx)。就好像他们不愿意程序员使用它一样。
    • 一点评论:似乎 QueueUserWorkItem 在 windows XP 下运行良好。从性能的角度来看,这种方法效果很好。我不会说我可以在没有性能损失的情况下减少到“一个数据批处理”,但我当然可以减少到 4 或 8 个批处理。与之前的 512 要求相比,这是一个很大的进步(由于线程创建开销)。现在,我只需要确保等待所有工作完成(即使有信号事件,我有时也会有一项未完成的工作)。 WaitForMultipleObjects() 肯定会有所帮助,只要我知道句柄数组在哪里...
    • 好的,我猜对了,终于。我已经补充了事件等待,这大致使我接近作业完成的结尾,并带有一个验证循环,该循环检查一些专用于作业结束信号的字段,在 2 次检查之间使用 Sleep(0);有用。由于 Sleep(0) 在下次尝试前最多 10 毫秒放弃,因此会有轻微的性能损失,但它可以工作。然而,它看起来很丑,就像一个黑客。我必须强调,我觉得为了检查工作结束而追求这种奇怪的算法是不正常的。似乎 MS 只是忘记了一个明显的 C API 调用 WaitForAll,它是在 c# 和 C++ 中提供的
    【解决方案3】:

    考虑使用CreateThreadpool(),而不是自己实现。操作系统会为您完成工作,您不必担心是否正确。

    【讨论】:

    • 听起来是个好建议;但是,我当前的代码相当简单,我理解它是如何工作的。另一方面,ThreadPools 看起来神秘而复杂。也许我需要的只是一个简单的例子,因为微软(msdn.microsoft.com/en-us/library/ms686980%28v=VS.85%29.aspx)提供的那个真的很可怕。
    猜你喜欢
    • 1970-01-01
    • 2014-06-17
    • 2012-05-20
    • 1970-01-01
    • 2013-03-08
    • 2013-07-20
    • 1970-01-01
    • 2023-03-22
    • 1970-01-01
    相关资源
    最近更新 更多