5

我目前正在为现有应用程序编写基于 Web 服务的前端。为此,我使用了WCF LOB 适配器 SDK,它允许创建自定义 WCF 绑定,将外部数据和操作公开为 Web 服务。

SDK 提供了一些接口来实现,并且它们的一些方法是有时间限制的:实现预期在指定的时间跨度内完成其工作或抛出TimeoutException

调查使我想到了“实现 C# 通用超时”这个问题,它明智地建议使用工作线程。有了这些知识,我可以写:

public MetadataRetrievalNode[] Browse(string nodeId, int childStartIndex,
    int maxChildNodes, TimeSpan timeout)
{
    Func<MetadataRetrievalNode[]> work = () => {
        // Return computed metadata...
    };

    IAsyncResult result = work.BeginInvoke(null, null);
    if (result.AsyncWaitHandle.WaitOne(timeout)) {
        return work.EndInvoke(result);
    } else {
        throw new TimeoutException();
    }
}

但是,对于超时时如何处理工作线程的共识尚不清楚。可以像上面的代码一样忘记它,或者可以中止它:

public MetadataRetrievalNode[] Browse(string nodeId, int childStartIndex,
    int maxChildNodes, TimeSpan timeout)
{
    Thread workerThread = null;
    Func<MetadataRetrievalNode[]> work = () => {
        workerThread = Thread.CurrentThread;
        // Return computed metadata...
    };

    IAsyncResult result = work.BeginInvoke(null, null);
    if (result.AsyncWaitHandle.WaitOne(timeout)) {
        return work.EndInvoke(result);
    } else {
        workerThread.Abort();
        throw new TimeoutException();
    }
}

现在,中止线程被广泛认为是错误的。它会破坏正在进行的工作、泄漏资源、混淆锁定,甚至不能保证线程实际上会停止运行。也就是说,HttpResponse.Redirect()每次调用它都会中止一个线程,而 IIS 似乎对此非常满意。也许它已经准备好以某种方式处理它。我的外部应用程序可能不是。

另一方面,如果我让工作线程运行,除了资源争用增加(池中可用线程减少)之外,内存不会泄漏,因为work.EndInvoke()永远不会被调用?MetadataRetrievalNode[]更具体地说,返回的数组不会work永远存在吗?

这只是选择两个弊端中较小的一个问题,还是有办法不中止工作线程并仍然回收所使用的内存BeginInvoke()

4

2 回答 2

6

好吧,首先Thread.Abort它并不像以前那么糟糕。在 2.0 中对 CLR 进行了几项改进,修复了与中止线程有关的几个主要问题。请注意,它仍然很糟糕,因此避免它是最好的做法。如果您必须求助于中止线程,那么至少您应该考虑拆除源自中止的应用程序域。在大多数情况下,这将是令人难以置信的侵入性,并且不会解决非托管资源可能的损坏。

除此之外,在这种情况下中止将产生其他影响。最重要的是您正在尝试中止ThreadPool线程。我真的不确定最终结果会是什么,并且可能会有所不同,具体取决于正在使用的框架版本。

最好的做法是让您的Func<MetadataRetrievalNode[]>委托在安全点轮询一个变量,看看它是否应该自行终止执行。

public MetadataRetrievalNode[] Browse(string nodeId, int childStartIndex, int maxChildNodes, TimeSpan timeout)
{
    bool terminate = false;

    Func<MetadataRetrievalNode[]> work = 
      () => 
      {
        // Do some work.

        Thread.MemoryBarrier(); // Ensure a fresh read of the terminate variable.
        if (terminate) throw new InvalidOperationException();

        // Do some work.

        Thread.MemoryBarrier(); // Ensure a fresh read of the terminate variable.
        if (terminate) throw new InvalidOperationException();

        // Return computed metadata...
      };

    IAsyncResult result = work.BeginInvoke(null, null);
    terminate = !result.AsyncWaitHandle.WaitOne(timeout);
    return work.EndInvoke(result); // This blocks until the delegate completes.
}

棘手的部分是如何处理代理内部的阻塞调用。terminate显然,如果委托处于阻塞调用的中间,则无法检查标志。但是,假设阻塞调用是从罐装 BCL 等待机制之一( 、 等)启动的WaitHandle.WaitOneMonitor.Wait那么您可以使用Thread.Interrupt“戳”它并立即解除阻塞。

于 2010-12-21T21:46:43.037 回答
1

答案取决于您的工作线程正在执行的工作类型。我的猜测是它正在使用数据连接等外部资源。Thread.Abort() 在任何线程使用非托管资源的钩子工作的情况下确实是邪恶的,无论包装得多么好。

基本上,您希望您的服务在超时时放弃。此时,理论上,调用者不再关心线程要花多长时间;它只关心它“太长”,应该继续。除非工作线程的运行方法中出现错误,否则它将最终结束;调用者不再关心何时,因为它不再等待。

现在,如果线程超时的原因是因为它陷入了无限循环,或者被告知要在其他操作(如服务调用)上永远等待,那么您有一个问题需要修复,但修复不是杀死线程。这类似于在你在车里等的时候把你的孩子送到杂货店买面包。如果你的孩子在你认为应该花 5 分钟的时候在商店里花 15 分钟,你最终会好奇,进去看看他们在做什么。如果这不是您认为他们应该做的事情,就像他们一直在看锅碗瓢盆一样,您可以“纠正”他们未来的行为。如果你进去看到你的孩子排着长长的结账队伍,那么你就开始等待更长的时间。在这两种情况下,你都不应该按下引爆他们穿着的爆炸背心的按钮;这只会造成很大的混乱,可能会干扰下一个孩子以后做同样事情的能力。

于 2010-12-21T21:41:51.913 回答