65.9K
CodeProject 正在变化。 阅读更多。
Home

带泛型的可控通知线程队列

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.53/5 (9投票s)

2006 年 8 月 3 日

CPOL

8分钟阅读

viewsIcon

64579

downloadIcon

621

System.Threading.ThreadPool 的一种替代方案。一个可控队列,用于异步执行操作。

引言

您是否曾经需要异步运行大量方法?您是否需要同时限制正在运行的数量?您是否需要对其进行交互式控制?那么,NotifyingThreadQueue 可能正是您所需要的。

比较

微软在提供多种方式满足我们需求方面做得非常好,但有时很难知道该使用哪种。在我之前的文章中,我收到了很多关于这个问题的评论。我归咎于我糟糕的表达。本节旨在解决这个问题。

System.Threading.Thread

优点

  • 从 1.1 (1.0?) 开始就有。
  • 很多人都非常熟悉它。

缺点

  • .NET 2.0 中已弃用挂起和恢复功能。
  • 您需要自己设置回调来获取错误和完成信息。
    警告:线程上的未捕获错误很可能会(总是?)消失,同时终止线程。
  • 它一次异步执行一个操作。当您需要大量操作时,您需要大量线程。

System.Threading.ThreadPool

这是最接近我所做内容的。

优点

  • System.Threading.Thread 的所有缺点。
  • 允许排队许多操作(大致按顺序)。它们最终都会完成。当您将超过 ThreadPool 线程的最大数量(25?)放入 ThreadPool 时,其他线程会等待,直到其中一个线程可用。
  • 重用旧线程以避免重新分配的成本(??)。我记得听过这个,但我没有找到文字记录,也不知道如何验证。

缺点

  • 您无法“暂停/继续”执行。已弃用挂起和恢复功能。
    注意:当我提到暂停和继续时,我不是直接指挂起和恢复。而是指暂停队列本身的能力。也就是说,您让当前线程完成,但剩余的线程会“暂停”,直到您希望它们“继续”。
  • 您需要自己设置回调来获取错误和完成信息。

System.Windows.Forms.BackgroundWorker

优点

  • 它是您 VS2005 工具箱中的一个控件。拖放即可。
  • 它内置了完成和进度事件,但没有错误事件。

缺点

  • .NET 2.0 仅支持。这不算什么大问题,除非您根本不能使用任何 2.0 代码。
  • 它一次异步执行一个操作。当您需要大量操作时,您需要大量 BackgroundWorkers

"Gang of Four" 委托模式

我实际上不拥有这本书,所以无法保证其价值,但我听说它很好。之所以在这里提及,是因为我拥有的另一本书引用了他们的模式,认为它与这个更广为人知的模式相似。

"Thilmany" 通知线程管理器

参见参考文献。我拥有这本书。我喜欢它。

优点

  • 支持同时进行大量操作。
  • 预置了错误和完成事件,但没有进度事件。这似乎很奇怪,因为他们希望这是一个演示层,却没有进度事件。嗯。

缺点

  • 只能执行一种类型的操作。在排队时没有默认操作,可以选择不同的操作。
  • 使用委托作为回调而不是事件。我认为这是因为他们希望这是一个演示层模式,并且他们希望避免 2.0 的跨线程问题。
  • 一旦将线程交给管理器,就无法停止/暂停/继续这些线程。
  • 无法控制线程的最大数量。

NotifyingThreadQueue

优点

  • 支持同时进行大量操作。
  • 预置了错误和完成事件,但没有进度事件。您可以相当容易地添加进度,但请注意跨线程异常。(稍后将详细介绍。)
  • 一旦将线程交给管理器,就可以停止/暂停/继续这些线程。
  • 您可以指定一个默认操作,并在排队时为其提供一个不同的操作。
  • 您可以控制线程的最大数量。

缺点

  • .NET 2.0 仅支持。我使用了泛型,因为它们允许强类型。您可以移除它们,它应该可以在 1.1 中工作。

队列操作

NotifyingThreadQueue 上有几个操作可以帮助您以一种较为交互的方式确定队列的状态。

停止

这会告诉 NotifyingThreadQueue 不再接受更多工作,并删除所有等待但未开始的工作。它会让它已经启动的线程完成其操作。它不会调用 Abort()。当您调用此操作时,队列将调用其 ThreadError 事件。这是一个特殊情况。该事件包含未处理的对象以及异常“System.Threading.ThreadStateException("the Queue is stopping. No processing done")”。我这样做是为了让您能够回收未处理的对象。

Continue

这会告诉 NotifyingThreadQueue 继续处理队列中的项目。一旦调用了 Stop(),它就不会继续。

Pause

这会告诉 NotifyingThreadQueue 继续接收要入队的项,但直到调用 Continue() 之前,都不会执行任何更多项。您不能 Pause() Stop() 命令。

队列事件

QueueStateChanged

我让队列有了状态,以便添加队列操作。添加这个是因为它实际上并不需要太多努力。

  • QueueState.Idle:准备接受并运行操作。目前没有正在运行的操作。
  • QueueState.Running:准备接受并运行操作。目前正在运行操作,因此操作可能会排队,直到一些当前正在运行的操作完成。
  • QueueState.Pausing:准备接受操作,但不会运行任何一个。目前正在运行操作,但在收到 Continue() 命令之前,不会再运行任何操作。
  • QueueState.Paused:准备接受操作,但不会运行任何一个。目前没有正在运行的操作。
  • QueueState.Stopping:不会接受新操作。所有已入队的 are being thrown away。目前正在运行操作。当所有当前操作完成后,队列将返回到 QueueState.Idle 状态。

ThreadFinished

当操作完成时,最好能以某种方式通知您。这个事件就是为此而设的。

ThreadError

ThreadError 事件有两个目的。首先是通知您操作中存在错误。这对我来说很重要,因为这通常(总是?)会杀死线程,并且在 VS 中没有任何错误报告。它还用于告诉您,由于队列已停止,该对象未被处理。我认为这是一个不错的功能。这允许您回收该对象,如果您真的想对它做些什么的话。

值得注意的事项

private void RunOpp(object o)
{
KeyValuePair<T, QueueOperationHandler<T>> kvp = 
	(KeyValuePair<T, QueueOperationHandler<T>>)o;
...
}

天哪。在 .NET 2.0 中进行类型转换!我说是传闻,传闻!即使在 .NET 2.0 中,也没有一个方法可以启动一个接受强类型参数的线程。即使是新的

ParameterizedThreadStart(void(object) target). 

有人可能会认为这可以被做成泛型,但显然没有。这也不是太糟糕,因为该方法是 private 并且放入其中的值总是来自强类型队列。所以它实际上并不算太糟。

注意事项

我将 NotifyingThreadQueue 设计为作为中间层组件使用。但是,我在演示中使用它作为演示层组件。这意味着存在跨线程问题。如果您不熟悉这一点,这是 .NET 2.0 的一个特性。微软决定不允许一个线程访问在不同线程上创建的 System.Windows.Forms.Control。有两种解决方案:

<yourcontrolname>.CheckForIllegalCrossThreadCalls = false; 

CheckForIllegalCrossThreadCalls 是所有 System.Windows.Forms.Controls 上的一个 static 布尔值。基本上,它表示忽略所有这类错误并继续。这可行,但如果您遇到很多这类错误,那么您的应用程序可能会明显变慢。如果您只有很少这类错误,那么这是一个快速的修复方法。
注意:您是否正在更新进度条?您将每更新一次都会遇到一个错误。
此类错误的其它实例包括设置任何控件的 Text 字段。我遇到的一个例子是调用我的 <listviews>.Items.Add();

使用委托并在创建控件的线程上调用该调用。

private delegate void AddCallback(ListView lv, string value);
private void AddLVIC(ListView lv, string value)
{
lv.Items.Add(value);
}
this.Invoke(new AddCallback(AddLVIC), new object[] { lvStatus, qs.ToString() });

这是更好的做法。但是,如果您有很多更新或很多不同类型的更新,这会变得很乏味。这是我设计的真正缺点。但正如我所说,我将在其他地方使用它。

静默跨线程异常丢弃。我认识到您可能遇到跨线程异常问题,因此我决定捕获并静默丢弃它。这是一个双刃剑QueueStateChangedInternalThreadFinishedInternalThreadErrorInternal 是罪魁祸首。如果我发现我真的不喜欢这一点,我可能会提供另一个构造函数。这取决于我自己的使用情况。

演示应用程序

Sample screenshot

演示应用程序没什么花哨的。上面的 6 个按钮可以让您尝试一两个运算符和最大线程数。下面的 4 个按钮用于交互式查看,这样您就可以看到开始更改,并且错误会被捕获和处理。您还可以暂停、停止和继续操作,看看您是否喜欢这种流程。

参考文献

  • .NET 模式:架构、设计和过程。Christian Thilmany。Addison Wesley。2004

历史

  • 2006 年 8 月 3 日:首次发布
© . All rights reserved.