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

可取消的线程池

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.52/5 (8投票s)

2004年9月13日

6分钟阅读

viewsIcon

98559

downloadIcon

924

一个具有中止和恢复功能的替代线程池。

引言

现代应用程序需要意识到远不止其自身上下文的东西,特别是,需要意识到它们所运行机器的电源限制。越来越多的用户在有电池限制的笔记本电脑上运行,我们应该编写程序,以便在电池耗尽时帮助他们节省电量。

ThreadPool 是一个很好的异步编程方案,但当您需要中止或暂停其中的线程时该怎么办?这个类使用了 ThreadPoolWaitItem 接口,允许您几乎完全替换它,并提供了额外的功能以允许中止和重启。

背景

我在笔记本电脑上工作,一天结束时,我会让它休眠。回到家后,我会连接到另一个网络适配器上的不同网络。不幸的是,当我在一个带有后台异步 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 接口不同,后者:

  1. 可以在构造后更改,
  2. 始终有最少数量的线程在运行。

有谁能想到这些差异会引起任何问题的场景吗?

历史

  • v0.2a - 2004年12月16日 - 重写以允许插件逻辑来控制线程启动。
  • V0.1a - 2004年9月13日。初始版本。
© . All rights reserved.