可取消的线程池






3.52/5 (8投票s)
2004年9月13日
6分钟阅读

98559

924
一个具有中止和恢复功能的替代线程池。
引言
现代应用程序需要意识到远不止其自身上下文的东西,特别是,需要意识到它们所运行机器的电源限制。越来越多的用户在有电池限制的笔记本电脑上运行,我们应该编写程序,以便在电池耗尽时帮助他们节省电量。
ThreadPool 是一个很好的异步编程方案,但当您需要中止或暂停其中的线程时该怎么办?这个类使用了 ThreadPool
的 WaitItem
接口,允许您几乎完全替换它,并提供了额外的功能以允许中止和重启。
背景
我在笔记本电脑上工作,一天结束时,我会让它休眠。回到家后,我会连接到另一个网络适配器上的不同网络。不幸的是,当我在一个带有后台异步 WebRequest
/WebResponse
的应用程序上工作时,恢复机器后,异常从 .NET 框架的深处抛出,而我根本无法将这个线程包装在 try
/catch
块中(可能,这与影响 Visual Studio 的问题是同一个)。我尝试改回同步调用,但这会阻塞 GUI,并且异常仍然出现。我需要一种方法,在暂停机器之前中止我的 WebRequest
,并以异步方式运行。
我决定最好的方法是在另一个线程中同步运行这个调用。一个线程可以在机器暂停前被中止。一个线程不会阻塞 GUI。一个线程甚至可以来自线程池。不幸的是,System.Threading.ThreadPool
没有提供对底层线程的访问,所以一旦线程启动就无法停止它。
最初,我并没有打算复制 ThreadPool
,我只是想要一种简单的方法来启动和停止在线程中运行的进程。那个版本接受一个委托,该委托在一个线程中启动并返回一个结果。然而,那个版本在我的应用程序中会同时启动 10-20 个线程,所以我考虑将工作排队,并使用单个线程来运行多个委托。在参考了 ThreadPool
的接口和文档后,我清楚地意识到我是在重新发明轮子;所以,我借鉴了 System.Threading.ThreadPool
接口的相关部分,并重新实现了它。
ThreadPool
接口中有一些方法与池内线程的数量有关,我选择不实现它们。根据 MSDN 文档,标准的 ThreadPool
会始终保持最小数量的线程在运行。我则选择在没有剩余工作时丢弃每一个线程,因此最终可能会一个运行的线程都没有。如果我每 30 分钟只使用 ThreadPool
进行 5 秒钟的处理,我认为没有必要让它们一直存在。更多关于这个话题的细节见下文。
更新
这个项目已经进行了大量的重写,包含了一个 ThreadStartEvaluator
抽象类。如果您希望更改启动和停止线程的逻辑,只需继承这个基类,并将您的新类传入线程池的构造函数即可。
使用代码
尽管您的代码从 System.Threading.ThreadPool
对象迁移到 CancellableThreadPool
后可以完美运行,但您应该意识到您的线程更有可能接收到中止信号。为了提供尽可能好的代码,我们需要用 try
/catch
块覆盖关键部分,以捕获 ThreadAbortException
。这些部分可以简单地执行已分配对象的清理工作,而不是仅仅依赖垃圾回收,或者可能会中止其他任务,例如现有的 WebRequest
/WebResponse
,例如:
public class ExampleThread()
{
WebRequest wr = ...;
public void ThreadMethod()
{
try
{
wr.GetResponse(...);
}
catch (ThreadAbortException e)
{
//Cancel all objects and reset so that we can be called again cleanly
wr.Dispose;
//Finish the Method ASAP.
return;
}
}
}
向新应用程序添加可取消的线程池
首先将 CancellableThreadPool
添加到您的主应用程序逻辑层(这可能是您的主窗体),并使用每个线程的最大排队项目数进行构造。
CancellableThreadPool _threads = new CancellableThreadPool(...);
如您所见,演示应用程序使用的默认最大队列长度为 2。如果您希望应用程序永远不将进程排队,而是总是分配一个新线程,请指定 0。或者,一个大的数字(例如,65535)会给您一个总是每次运行单个进程的队列,但如果您停止并重新启动,您不保证会以相同的顺序处理。
接下来,定义一个具有可用作 WaitCallback
委托签名的方法,例如:
public void Update(object state)
...
要启动一个进程,请使用以下代码,其中 demo
是持有该方法的对象的名称,
ThreadDemo demo = new ThreadDemo(...);
_threads.QueueWorkItem(new WaitCallback(demo.Update) );
在您的应用程序中支持线程
现在您的代码已经多线程运行了,一切都很顺利,直到您尝试更新您的 System.Windows.Form Gui
。这个例子通过从计时器刷新并从对象中拉取数据来避免这个问题,所有操作都来自创建 GUI 对象的线程。这不是一个理想的例子。
相反,我建议在您的 Form
中添加提供简单功能的方法。这个例子本可以包含
internal void ThreadStatus(ThreadDemo demo, string message)
然后,public
/internal
方法负责检查我们是否在正确的线程上,并将更新委托给一个 private
方法,即:
internal void ThreadStatus(ThreadDemo demo, string message)
{
if (this.InvokeRequired == true)
this.Invoke(new ThreadStatusDelegate(realThreadStatus),
new object[2] {demo,message});
else
realThreadStatus(demo, message);
}
private delegate void ThreadStatusDelegate(ThreadDemo demo, string status);
private void realThreadStatus(ThreadDemo demo, string message)
{
GetListItem(demo).Text = message;
}
代码功能
这个演示展示了一个 ThreadDemo
对象,它试图计数到 50。当重新启动时,它会查看其当前计数值并加上 50,这定义了它将停止计数的位置。如果在线程的中止和重启过程中发生错误,您将看到计数超过 50 的线程。然而,当检测到 ThreadAbortException
时,计数器会重置为零。这个场景旨在演示一种用于更复杂流程的重置方法。
CancellableThreadPool
主要的可复用代码在 CancellableThreadPool.cs 文件中。这个类的复杂性在于 ThreadRunner
方法。这个方法通过提取一个排队的工作项并在其中执行委托来工作。您负责处理委托内的错误。如果您让异常传播到您的控制范围之外,那么线程将终止以避免任何后续可能的并发症,并帮助推进垃圾回收。整个方法由这个类启动的线程运行。
ThreadStartEvaluator 和派生类
这段代码用于决定何时启动和停止线程。覆盖 EvaluateThreadStartStop
方法来决定何时应该启动和停止新线程。
ThreadRunInfo
这个类允许将状态对象传递给正在运行的线程,就像 System.Threading.ThreadPool
那样。
关注点
通过 ThreadStartEvaluatorbyQueueSize
的实现,正在运行的线程数量取决于队列中的项目数量。您可以使用构造函数设置每个线程的最大排队项目数,如上所述。这与 System.Threading.ThreadPool
接口不同,后者:
- 可以在构造后更改,
- 始终有最少数量的线程在运行。
有谁能想到这些差异会引起任何问题的场景吗?
历史
- v0.2a - 2004年12月16日 - 重写以允许插件逻辑来控制线程启动。
- V0.1a - 2004年9月13日。初始版本。