实现 C# 通用超时

Posted

技术标签:

【中文标题】实现 C# 通用超时【英文标题】:Implement C# Generic Timeout 【发布时间】:2010-09-22 21:18:38 【问题描述】:

我正在寻找实现一种通用方法的好主意,以使单行(或匿名委托)代码执行超时。

TemperamentalClass tc = new TemperamentalClass();
tc.DoSomething();  // normally runs in 30 sec.  Want to error at 1 min

我正在寻找一种可以在我的代码与气质代码(我无法更改)交互的许多地方优雅地实施的解决方案。

此外,如果可能的话,我希望停止执行有问题的“超时”代码。

【问题讨论】:

只是提醒任何查看以下答案的人:他们中的许多人使用 Thread.Abort 这可能非常糟糕。在您的代码中实现 Abort 之前,请阅读有关此的各种 cmet。有时它可能是合适的,但这种情况很少见。如果您不完全了解 Abort 的用途或不需要它,请实施以下不使用它的解决方案之一。它们是没有那么多选票的解决方案,因为它们不符合我的问题的需要。 有关 thread.Abort 的危险的详细信息,请阅读 Eric Lippert 的这篇文章:blogs.msdn.com/b/ericlippert/archive/2010/02/22/… 【参考方案1】:

好吧,您可以使用委托(BeginInvoke,使用回调设置标志 - 原始代码等待该标志或超时) - 但问题是很难关闭正在运行的代码。例如,杀死(或暂停)一个线程是危险的......所以我认为没有一种简单的方法可以稳健地做到这一点。

我会发布这个,但请注意它并不理想 - 它不会停止长时间运行的任务,并且不会在失败时正确清理。

    static void Main()
    
        DoWork(OK, 5000);
        DoWork(Nasty, 5000);
    
    static void OK()
    
        Thread.Sleep(1000);
    
    static void Nasty()
    
        Thread.Sleep(10000);
    
    static void DoWork(Action action, int timeout)
    
        ManualResetEvent evt = new ManualResetEvent(false);
        AsyncCallback cb = delegate evt.Set();;
        IAsyncResult result = action.BeginInvoke(cb, null);
        if (evt.WaitOne(timeout))
        
            action.EndInvoke(result);
        
        else
        
            throw new TimeoutException();
        
    
    static T DoWork<T>(Func<T> func, int timeout)
    
        ManualResetEvent evt = new ManualResetEvent(false);
        AsyncCallback cb = delegate  evt.Set(); ;
        IAsyncResult result = func.BeginInvoke(cb, null);
        if (evt.WaitOne(timeout))
        
            return func.EndInvoke(result);
        
        else
        
            throw new TimeoutException();
        
    

【讨论】:

我很高兴能杀死我身上染上红色的东西。它仍然比让它吃 CPU 周期直到下一次重新启动要好(这是 Windows 服务的一部分)。 @Marc:我是你的忠实粉丝。但是,这一次,我想知道,为什么你没有使用 TheSoftwareJedi 提到的 result.AsyncWaitHandle。在 AsyncWaitHandle 上使用 ManualResetEvent 有什么好处? @Anand 好吧,这是几年前的事了,所以我无法凭记忆回答——但“易于理解”在线程代码中很重要【参考方案2】:

我现在刚刚取消了这个,所以它可能需要一些改进,但会做你想做的。这是一个简单的控制台应用程序,但演示了所需的原则。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;


namespace TemporalThingy

    class Program
    
        static void Main(string[] args)
        
            Action action = () => Thread.Sleep(10000);
            DoSomething(action, 5000);
            Console.ReadKey();
        

        static void DoSomething(Action action, int timeout)
        
            EventWaitHandle waitHandle = new EventWaitHandle(false, EventResetMode.ManualReset);
            AsyncCallback callback = ar => waitHandle.Set();
            action.BeginInvoke(callback, null);

            if (!waitHandle.WaitOne(timeout))
                throw new Exception("Failed to complete in the timeout specified.");
        
    


【讨论】:

不错。我唯一要补充的是,他可能更喜欢抛出 System.TimeoutException 而不仅仅是 System.Exception 哦,是的:我也会把它包装在它自己的类中。【参考方案3】:

这里真正棘手的部分是通过将执行程序线程从 Action 传递回可以中止的地方来终止长时间运行的任务。我通过使用包装的委托来实现这一点,该委托将要杀死的线程传递到创建 lambda 的方法中的局部变量中。

我提交此示例,供您欣赏。您真正感兴趣的方法是 CallWithTimeout。 这将通过中止它并吞下 ThreadAbortException 来取消长时间运行的线程

用法:

class Program


    static void Main(string[] args)
    
        //try the five second method with a 6 second timeout
        CallWithTimeout(FiveSecondMethod, 6000);

        //try the five second method with a 4 second timeout
        //this will throw a timeout exception
        CallWithTimeout(FiveSecondMethod, 4000);
    

    static void FiveSecondMethod()
    
        Thread.Sleep(5000);
    

做这项工作的静态方法:

    static void CallWithTimeout(Action action, int timeoutMilliseconds)
    
        Thread threadToKill = null;
        Action wrappedAction = () =>
        
            threadToKill = Thread.CurrentThread;
            try
            
                action();
            
            catch(ThreadAbortException ex)
               Thread.ResetAbort();// cancel hard aborting, lets to finish it nicely.
            
        ;

        IAsyncResult result = wrappedAction.BeginInvoke(null, null);
        if (result.AsyncWaitHandle.WaitOne(timeoutMilliseconds))
        
            wrappedAction.EndInvoke(result);
        
        else
        
            threadToKill.Abort();
            throw new TimeoutException();
        
    


【讨论】:

为什么会出现 catch(ThreadAbortException)? AFAIK 你不能真正捕捉到 ThreadAbortException (它会在 catch 块离开后重新抛出)。 Thread.Abort() 使用起来非常危险,它不应该与常规代码一起使用,只有保证安全的代码才应该中止,例如 Cer.Safe 的代码,使用受约束的执行区域和安全句柄。任何代码都不应该这样做。 虽然 Thread.Abort() 很糟糕,但它远没有进程失控并使用 PC 拥有的每个 CPU 周期和内存字节那么糟糕。但是您向可能认为此代码有用的任何其他人指出潜在问题是正确的。 我不敢相信这是被接受的答案,一定不是有人在这里阅读 cmets,或者答案在 cmets 之前被接受并且那个人没有检查他的回复页面。 Thread.Abort 不是解决方案,它只是您需要解决的另一个问题! 你是不阅读 cmets 的人。正如上面chilltemp所说,他正在调用他无法控制的代码 - 并希望它中止。如果他希望它在他的进程中运行,他除了 Thread.Abort() 之外别无选择。你说得对,Thread.Abort 很糟糕——但就像chilltemp 所说,其他事情更糟!【参考方案4】:

这就是我的做法:

public static class Runner

    public static void Run(Action action, TimeSpan timeout)
    
        IAsyncResult ar = action.BeginInvoke(null, null);
        if (ar.AsyncWaitHandle.WaitOne(timeout))
            action.EndInvoke(ar); // This is necesary so that any exceptions thrown by action delegate is rethrown on completion
        else
            throw new TimeoutException("Action failed to complete using the given timeout!");
    

【讨论】:

这不会停止执行任务 并非所有任务都可以安全停止,各种问题都可能到来,死锁、资源泄漏、状态损坏……一般情况下不应该这样做。【参考方案5】:

对 Pop Catalin 的最佳答案的一些小改动:

Func 代替 Action 超时值错误引发异常 在超时情况下调用 EndInvoke

已添加重载以支持信号工作者取消执行:

public static T Invoke<T> (Func<CancelEventArgs, T> function, TimeSpan timeout) 
    if (timeout.TotalMilliseconds <= 0)
        throw new ArgumentOutOfRangeException ("timeout");

    CancelEventArgs args = new CancelEventArgs (false);
    IAsyncResult functionResult = function.BeginInvoke (args, null, null);
    WaitHandle waitHandle = functionResult.AsyncWaitHandle;
    if (!waitHandle.WaitOne (timeout)) 
        args.Cancel = true; // flag to worker that it should cancel!
        /* •————————————————————————————————————————————————————————————————————————•
           | IMPORTANT: Always call EndInvoke to complete your asynchronous call.   |
           | http://msdn.microsoft.com/en-us/library/2e08f6yc(VS.80).aspx           |
           | (even though we arn't interested in the result)                        |
           •————————————————————————————————————————————————————————————————————————• */
        ThreadPool.UnsafeRegisterWaitForSingleObject (waitHandle,
            (state, timedOut) => function.EndInvoke (functionResult),
            null, -1, true);
        throw new TimeoutException ();
    
    else
        return function.EndInvoke (functionResult);


public static T Invoke<T> (Func<T> function, TimeSpan timeout) 
    return Invoke (args => function (), timeout); // ignore CancelEventArgs


public static void Invoke (Action<CancelEventArgs> action, TimeSpan timeout) 
    Invoke<int> (args =>  // pass a function that returns 0 & ignore result
        action (args);
        return 0;
    , timeout);


public static void TryInvoke (Action action, TimeSpan timeout) 
    Invoke (args => action (), timeout); // ignore CancelEventArgs

【讨论】:

Invoke(e => // ... if (error) e.Cancel = true; return 5; , TimeSpan.FromSeconds(5)); 值得指出的是,在这个答案中,“超时”方法会一直运行,除非它可以修改为在标记为“取消”时礼貌地选择退出。 David,这就是专门创建 CancellationToken 类型 (.NET 4.0) 来解决的问题。在这个答案中,我使用了 CancelEventArgs 以便工作人员可以轮询 args.Cancel 以查看它是否应该退出,尽管这应该使用 .NET 4.0 的 CancellationToken 重新实现。 一个使用说明让我困惑了一会儿:如果你的函数/动作代码在超时后可能会抛出异常,你需要两个 try/catch 块。您需要围绕调用 Invoke 进行一次尝试/捕获以捕获 TimeoutException。您需要在 Function/Action 中使用一秒钟来捕获并吞下/记录超时引发后可能发生的任何异常。否则应用程序将因未处理的异常而终止(我的用例是在比 app.config 中指定的更严格的超时时间上 ping 测试 WCF 连接) 绝对 - 因为函数/动作中的代码可以抛出,它必须在 try/catch 中。按照惯例,这些方法不会尝试尝试/捕获函数/动作。捕获和丢弃异常是一个糟糕的设计。与所有异步代码一样,由方法的用户来尝试/捕获。【参考方案6】:

我们在生产环境中大量使用这样的代码n:

var result = WaitFor<Result>.Run(1.Minutes(), () => service.GetSomeFragileResult());

实现是开源的,即使在并行计算场景中也能高效工作,并且作为Lokad Shared Libraries 的一部分提供

/// <summary>
/// Helper class for invoking tasks with timeout. Overhead is 0,005 ms.
/// </summary>
/// <typeparam name="TResult">The type of the result.</typeparam>
[Immutable]
public sealed class WaitFor<TResult>

    readonly TimeSpan _timeout;

    /// <summary>
    /// Initializes a new instance of the <see cref="WaitForT"/> class, 
    /// using the specified timeout for all operations.
    /// </summary>
    /// <param name="timeout">The timeout.</param>
    public WaitFor(TimeSpan timeout)
    
        _timeout = timeout;
    

    /// <summary>
    /// Executes the spcified function within the current thread, aborting it
    /// if it does not complete within the specified timeout interval. 
    /// </summary>
    /// <param name="function">The function.</param>
    /// <returns>result of the function</returns>
    /// <remarks>
    /// The performance trick is that we do not interrupt the current
    /// running thread. Instead, we just create a watcher that will sleep
    /// until the originating thread terminates or until the timeout is
    /// elapsed.
    /// </remarks>
    /// <exception cref="ArgumentNullException">if function is null</exception>
    /// <exception cref="TimeoutException">if the function does not finish in time </exception>
    public TResult Run(Func<TResult> function)
    
        if (function == null) throw new ArgumentNullException("function");

        var sync = new object();
        var isCompleted = false;

        WaitCallback watcher = obj =>
            
                var watchedThread = obj as Thread;

                lock (sync)
                
                    if (!isCompleted)
                    
                        Monitor.Wait(sync, _timeout);
                    
                
                   // CAUTION: the call to Abort() can be blocking in rare situations
                    // http://msdn.microsoft.com/en-us/library/ty8d3wta.aspx
                    // Hence, it should not be called with the 'lock' as it could deadlock
                    // with the 'finally' block below.

                    if (!isCompleted)
                    
                        watchedThread.Abort();
                    
        ;

        try
        
            ThreadPool.QueueUserWorkItem(watcher, Thread.CurrentThread);
            return function();
        
        catch (ThreadAbortException)
        
            // This is our own exception.
            Thread.ResetAbort();

            throw new TimeoutException(string.Format("The operation has timed out after 0.", _timeout));
        
        finally
        
            lock (sync)
            
                isCompleted = true;
                Monitor.Pulse(sync);
            
        
    

    /// <summary>
    /// Executes the spcified function within the current thread, aborting it
    /// if it does not complete within the specified timeout interval.
    /// </summary>
    /// <param name="timeout">The timeout.</param>
    /// <param name="function">The function.</param>
    /// <returns>result of the function</returns>
    /// <remarks>
    /// The performance trick is that we do not interrupt the current
    /// running thread. Instead, we just create a watcher that will sleep
    /// until the originating thread terminates or until the timeout is
    /// elapsed.
    /// </remarks>
    /// <exception cref="ArgumentNullException">if function is null</exception>
    /// <exception cref="TimeoutException">if the function does not finish in time </exception>
    public static TResult Run(TimeSpan timeout, Func<TResult> function)
    
        return new WaitFor<TResult>(timeout).Run(function);
    


这段代码还是有问题的,你可以试试这个小测试程序:

      static void Main(string[] args) 

         // Use a sb instead of Console.WriteLine() that is modifying how synchronous object are working
         var sb = new StringBuilder();

         for (var j = 1; j < 10; j++) // do the experiment 10 times to have chances to see the ThreadAbortException
         for (var ii = 8; ii < 15; ii++) 
            int i = ii;
            try 

               Debug.WriteLine(i);
               try 
                  WaitFor<int>.Run(TimeSpan.FromMilliseconds(10), () => 
                     Thread.Sleep(i);
                     sb.Append("Processed " + i + "\r\n");
                     return i;
                  );
               
               catch (TimeoutException) 
                  sb.Append("Time out for " + i + "\r\n");
               

               Thread.Sleep(10);  // Here to wait until we get the abort procedure
            
            catch (ThreadAbortException) 
               Thread.ResetAbort();
               sb.Append(" *** ThreadAbortException on " + i + " *** \r\n");
            
         

         Console.WriteLine(sb.ToString());
      
   

存在竞争条件。很明显,在调用 WaitFor&lt;int&gt;.Run() 方法后会引发 ThreadAbortException。我没有找到解决此问题的可靠方法,但是通过相同的测试,我无法重现 TheSoftwareJedi 接受的答案的任何问题。

【讨论】:

这是我实现的,它可以处理参数和返回值,这是我喜欢和需要的。谢谢瑞纳特 只是我们用来标记不可变类的一个属性(不变性由 Mono Cecil 在单元测试中验证) 这是一个等待发生的死锁(我很惊讶你还没有观察到它)。您对watchedThread.Abort() 的调用位于锁内,也需要在finally 块中获取。这意味着当 finally 块等待锁时(因为 watchThread 在 Wait() 返回和 Thread.Abort() 之间有它),watchThread.Abort() 调用也将无限期地阻塞等待 finally 完成(它永远不会)。 Therad.Abort() 可以阻止代码的受保护区域正在运行 - 导致死锁,请参阅 - msdn.microsoft.com/en-us/library/ty8d3wta.aspx trickdev,非常感谢。出于某种原因,死锁似乎很少发生,但我们仍然修复了代码:-) 如何调用 void 类型的函数?【参考方案7】:

使用 Thread.Join(int timeout) 怎么样?

public static void CallWithTimeout(Action act, int millisecondsTimeout)

    var thread = new Thread(new ThreadStart(act));
    thread.Start();
    if (!thread.Join(millisecondsTimeout))
        throw new Exception("Timed out");

【讨论】:

这会通知调用方法有问题,但不会中止有问题的线程。 我不确定这是否正确。从文档中不清楚当 Join 超时过去时工作线程会发生什么。

以上是关于实现 C# 通用超时的主要内容,如果未能解决你的问题,请参考以下文章

如何在 C# 中编写实现给定接口的通用容器类?

C# 配合 Easyui DataGrid 实现增删改查 通用模板

java 从零开始手写 RPC (07)-timeout 超时处理

多映射通用集合类(C#实现)--支持一键多值存储

C# 给某个方法设定执行超时时间

如何在 C# 中等待单个事件,带有超时和取消