实现C#通用超时


157

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

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

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

另外,如果可能的话,我想阻止令人讨厌的“超时”代码进一步执行。


46
提醒所有查看以下答案的人:很多人使用Thread.Abort,这可能非常糟糕。在您的代码中实现Abort之前,请阅读有关此内容的各种注释。有时它是适当的,但是很少见。如果您不完全了解Abort的用途或不需要它,请实施以下不使用它的解决方案之一。它们是投票数量不多的解决方案,因为它们不符合我的问题的需求。
chilltemp

感谢您的咨询。+1票。
QueueHammer 2010年

7
:有关Thread.Abort的的危害的详细信息,请阅读这篇文章从埃里克利珀blogs.msdn.com/b/ericlippert/archive/2010/02/22/...
JohnW

Answers:


95

这里真正棘手的部分是通过将执行程序线程从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();
        }
    }

}

3
为什么要抓住(ThreadAbortException)?抱歉,您不能真正捕获ThreadAbortException(在离开catch块后将其重新抛出)。
csgero

12
Thread.Abort()的使用非常危险,不应与常规代码一起使用,仅应中止保证安全的代码,例如Cer.Safe,使用受限执行区和安全句柄的代码。不应为任何代码完成此操作。
Pop Catalin's

12
尽管Thread.Abort()很糟糕,但它几乎没有进程失控并且使用PC拥有的每个CPU周期和内存字节那样严重。但是您应该向可能认为此代码有用的任何其他人指出潜在的问题。
chilltemp

24
我无法相信这是被接受的答案,一定不能在这里阅读评论,或者在评论之前已经接受了答案,并且该人没有检查他的回复页面。Thread.Abort不是解决方案,这只是您需要解决的另一个问题!
拉瑟五世卡尔森

18
您是未读评论的人。正如chilltemp所说,他正在调用自己无法控制的代码-并希望其中止。如果他希望Thread.Abort()在进程中运行,他别无选择。您认为Thread.Abort不好是对的-但就像chilltemp所说,其他情况更糟!
TheSoftwareJedi,2009年

73

我们在生产中大量使用这样的代码

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

实施是开源的,即使在并行计算方案中也可以有效地工作,并且可以作为Lokad共享库的一部分使用

/// <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="WaitFor{T}"/> 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<int>.Run()在调用该方法之后,可能会引发ThreadAbortException 。我没有找到解决此问题的可靠方法,但是通过相同的测试,我无法对TheSoftwareJedi接受的答案提出任何问题。

在此处输入图片说明


3
这是我实现的,它可以处理我喜欢和需要的参数和返回值。感谢Rinat
Gabriel Mongeon

7
什么是[不可变]?
raklos 2011年

2
只是我们用来标记不可变类的属性(不可变性已通过Mono Cecil在单元测试中进行了验证)
Rinat Abdullin

9
这是一个等待发生的僵局(我很惊讶您还没有观察到它)。您对watchedThread.Abort()的调用位于一个锁内,该锁也需要在finally块中获取。这意味着,当finally块正在等待锁定时(因为watchedThread在Wait()返回和Thread.Abort()之间拥有了锁),watchedThread.Abort()调用也将无限期地等待finally完成(它永远不会)。Therad.Abort()如果可以的代码的受保护区域运行方框-引起死锁,见- msdn.microsoft.com/en-us/library/ty8d3wta.aspx
trickdev

1
特里普德夫,非常感谢。出于某种原因,似乎很少发生死锁,但是尽管如此,我们仍然修复了代码:-)
Joannes Vermorel 2011年

15

好吧,您可以使用委托(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();
        }
    }

2
我很高兴杀死掉对我发红的东西。它仍然比让它吃掉CPU周期好直到下次重新启动(这是Windows服务的一部分)更好。
chilltemp

@马克:我是你的忠实粉丝。但是,这一次,我想知道为什么您不使用TheSoftwareJedi提到的result.AsyncWaitHandle。在AsyncWaitHandle上使用ManualResetEvent有什么好处?
Anand Patel

1
@Anand好吧,这是几年前的事,所以我无法从内存中回答-但是“易于理解”在线程代码中非常重要
Marc Gravell

13

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
}

调用(e => {// ...如果(错误)e.Cancel = true;返回5;},TimeSpan.FromSeconds(5));
乔治·齐奥科斯

1
值得指出的是,在此答案中,“超时”方法保持运行状态,除非可以对其进行修改以在标记为“取消”时礼貌地选择退出。
David Eison 2011年

大卫,那就是专门创建CancellationToken类型(.NET 4.0)来解决的。在此答案中,我使用了CancelEventArgs,以便工作人员可以轮询args.Cancel以查看是否应退出,尽管应使用.NET 4.0的CancellationToken重新实现。
乔治·齐奥科斯

对此的使用说明使我有些困惑:如果您的功能/操作代码在超时后可能引发异常,则需要两个try / catch块。您需要在对Invoke的调用中进行一次try / catch才能捕获TimeoutException。您需要在功能/动作内部添加一秒钟,以捕获并吞下/记录超时抛出后可能发生的任何异常。否则,应用程序将以未处理的异常终止(我的用例是在比app.config中指定的更严格的超时上ping测试WCF连接)
法定

绝对-由于函数/操作中的代码可以抛出,因此它必须在try / catch中。按照惯例,这些方法不会尝试尝试/捕获功能/动作。捕获并丢弃异常是一个糟糕的设计。与所有异步代码一样,方法的用户可以尝试/捕获。
乔治·齐奥科斯

10

这是我的方法:

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!");
    }
}

3
这不会停止执行任务
TheSoftwareJedi

2
并非所有任务都可以安全地停止,各种问题都可以到达,出现死锁,资源泄漏,状态破坏……一般情况下不应该这样做。
Pop Catalin

7

我现在将其删除,因此可能需要一些改进,但是会做您想做的。这是一个简单的控制台应用程序,但演示了所需的原理。

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.");
        }
    }

}

1
真好 我唯一要补充的是,他可能更愿意抛出System.TimeoutException而不是System.Exception
Joel Coehoorn

哦,是的,我也将其包装在自己的类中。
Joel Coehoorn

2

怎样使用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");
}

1
这将通知调用方法有问题,但不会异常终止线程。
chilltemp

1
我不确定这是正确的。从文档中不清楚当Join超时过去时工作线程会发生什么。
马修·劳
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.