实现C#通用超时

chi*_*emp 157 c# multithreading asynchronous timeout c#-3.0

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

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

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

另外,如果可能的话,我想让违规的"超时"代码停止执行.

The*_*edi 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);
    }
Run Code Online (Sandbox Code Playgroud)

执行工作的静态方法:

    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();
        }
    }

}
Run Code Online (Sandbox Code Playgroud)

  • 我不敢相信这是接受的答案,有人不得在这里阅读评论,或者在评论之前接受了答案,并且该人没有查看他的回复页面.Thread.Abort不是解决方案,它只是你需要解决的另一个问题! (24认同)
  • 你是那个不读评论的人.正如上面所述的chilltemp所说,他正在调用他无法控制的代码 - 并希望它能够中止.除了Thread.Abort()之外,他没有其他选择,如果他希望在他的进程中运行它.你是对的,Thread.Abort很糟糕 - 但就像chilltemp说的那样,其他事情更糟糕! (18认同)
  • 虽然Thread.Abort()很糟糕,但它并不像失控的进程那么糟糕,而是使用PC拥有的每个CPU周期和内存字节.但是你可能会向其他可能认为此代码有用的人指出潜在的问题. (12认同)
  • Thread.Abort()使用起来非常危险,它不应该与常规代码一起使用,只应该中止保证安全的代码,例如Cer.Safe代码,使用受约束的执行区域和安全句柄.任何代码都不应该这样做. (11认同)
  • 为什么要捕获(ThreadAbortException)?AFAIK你无法真正捕获一个ThreadAbortException(它将在catch块停止后重新抛出). (3认同)
  • @TheSoftwareJedi:这是老帖子,但我有一点意见.您假设ThreadPool线程将立即拾取BeginInvoke工作项.该假设将在线程threadToKill.Abort()处的NullReferenceException中断.当ThreadPool上有一个加载(更多的项目排队然后它可以处理)并且你的result.AsyncWaitHandle.WaitOne(timeoutMilliseconds))在ThreadPool选择你的项目之前超时时会发生这种情况.在这种情况下,变量threadToKill保持未初始化状态. (2认同)

Rin*_*lin 72

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

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

实现是开源的,即使在并行计算场景中也能高效工作,并且可作为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);
    }
}
Run Code Online (Sandbox Code Playgroud)

这段代码仍然有问题,你可以试试这个小测试程序:

      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());
      }
   }
Run Code Online (Sandbox Code Playgroud)

有竞争条件.很明显,WaitFor<int>.Run()在调用方法之后会引发ThreadAbortException .我没有找到一种可靠的方法来解决这个问题,但是使用相同的测试,我无法通过TheSoftwareJedi接受的答案重现任何问题.

在此输入图像描述

  • 这是一个等待发生的僵局(我很惊讶你还没有观察到它).你对watchedThread.Abort()的调用是在一个锁中,也需要在finally块中获取.这意味着当finally块正在等待锁定时(因为watchedThread在Wait()返回和Thread.Abort()之间),watchedThread.Abort()调用也将无限期地阻塞等待最终完成(它永不).如果受保护的代码区域正在运行,则可以阻止Therad.Abort() - 导致死锁,请参阅 - http://msdn.microsoft.com/en-us/library/ty8d3wta.aspx (9认同)
  • 什么是[永恒]? (7认同)
  • 更新:使用更正的代码修复帖子. (4认同)
  • 这是我实现的,它可以处理参数和返回值,我更喜欢和需要.谢谢Rinat (3认同)
  • 只是我们用来标记不可变类的属性(单元测试中Mono Cecil验证了不变性) (2认同)

Mar*_*ell 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();
        }
    }
Run Code Online (Sandbox Code Playgroud)

  • 我很高兴杀死一些对我不感兴趣的东西.它仍然比让它在下次重启之前吃CPU周期更好(这是Windows服务的一部分). (2认同)
  • result.AsyncWaitHandle可以使用,不需要手动复位 (2认同)

Geo*_*kos 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
}
Run Code Online (Sandbox Code Playgroud)


Pop*_*lin 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!");
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 这不会停止执行任务 (3认同)
  • 并非所有任务都可以安全停止,各种问题都可以到来,死锁,资源泄漏,国家腐败......在一般情况下不应该这样做. (2认同)

Jas*_*son 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.");
        }
    }

}
Run Code Online (Sandbox Code Playgroud)