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






4.53/5 (9投票s)
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() });
这是更好的做法。但是,如果您有很多更新或很多不同类型的更新,这会变得很乏味。这是我设计的真正缺点。但正如我所说,我将在其他地方使用它。
静默跨线程异常丢弃。我认识到您可能遇到跨线程异常问题,因此我决定捕获并静默丢弃它。这是一个双刃剑。QueueStateChangedInternal
、ThreadFinishedInternal
和 ThreadErrorInternal
是罪魁祸首。如果我发现我真的不喜欢这一点,我可能会提供另一个构造函数。这取决于我自己的使用情况。
演示应用程序

演示应用程序没什么花哨的。上面的 6 个按钮可以让您尝试一两个运算符和最大线程数。下面的 4 个按钮用于交互式查看,这样您就可以看到开始更改,并且错误会被捕获和处理。您还可以暂停、停止和继续操作,看看您是否喜欢这种流程。
参考文献
- .NET 模式:架构、设计和过程。Christian Thilmany。Addison Wesley。2004
历史
- 2006 年 8 月 3 日:首次发布