在实现时间受限的方法时,我应该中止工作线程还是让它运行?

时间:2010-12-21 20:47:18

标签: c# multithreading wcf-lob-adapter

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

SDK提供了一些要实现的接口,并且它们的一些方法是时间约束的:实现应该在指定的时间跨度内完成其工作或抛出TimeoutException

调查让我想到了问题“Implement C# Generic Timeout”,明智地建议使用工作线程。有了这些知识,我可以写:

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()使用的内存?

2 个答案:

答案 0 :(得分: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“戳”它并且应该立即解锁它。

答案 1 :(得分:1)

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

基本上,如果超时,您希望您的服务放弃。在这一点上,从理论上讲,调用者不再关心线程需要多长时间;它只关心它“太长”,应该继续前进。除非工作线程的运行方法存在错误,否则它最终会结束;呼叫者不再关心什么时候因为它不再等待了。

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