代码之家  ›  专栏  ›  技术社区  ›  chilltemp

实现C#通用超时

  •  158
  • chilltemp  · 技术社区  · 16 年前

    我正在寻找好的想法来实现一种通用的方法,让一行(或匿名委托)代码在超时的情况下执行。

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

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

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

    7 回复  |  直到 16 年前
        1
  •  96
  •   Milad TheSoftwareJedi    8 年前

    这里真正棘手的部分是通过将执行器线程从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();
            }
        }
    
    }
    
        2
  •  73
  •   Patrick from NDepend team    7 年前

    我们在生产中大量使用这样的代码 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="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());
          }
       }
    

    有一个种族条件。很明显,在方法之后可能会引发ThreadAbortException WaitFor<int>.Run() 正在被召唤。我并没有找到一种可靠的方法来解决这个问题,但通过同样的测试,我无法重现任何问题 TheSoftwareJedi 接受答案。

    enter image description here

        3
  •  15
  •   Marc Gravell    16 年前

    好吧,你可以用委托来做一些事情(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();
            }
        }
    
        4
  •  13
  •   George Tsiokos    16 年前

    Pop Catalin的精彩回答的一些小变化:

    • 函数而不是动作
    • 对错误的超时值抛出异常
    • 超时时调用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
    }
    
        5
  •  10
  •   Pop Catalin    16 年前

    我会这样做:

    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!");
        }
    }
    
        6
  •  7
  •   Jason Jackson    16 年前

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

    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.");
            }
        }
    
    }
    
        7
  •  2
  •   eacousineau eacousineau    16 年前

    使用Thread怎么样。加入(int超时)?

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