Implement C# Generic Timeout

后端 未结 7 1718
谎友^
谎友^ 2020-11-22 09:39

I am looking for good ideas for implementing a generic way to have a single line (or anonymous delegate) of code execute with a timeout.

TemperamentalClass t         


        
相关标签:
7条回答
  • 2020-11-22 10:02

    This is how I'd do it:

    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!");
        }
    }
    
    0 讨论(0)
  • 2020-11-22 10:05

    Well, you could do things with delegates (BeginInvoke, with a callback setting a flag - and the original code waiting for that flag or timeout) - but the problem is that it is very hard to shut down the running code. For example, killing (or pausing) a thread is dangerous... so I don't think there is an easy way to do this robustly.

    I'll post this, but note it is not ideal - it doesn't stop the long-running task, and it doesn't clean up properly on failure.

        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();
            }
        }
    
    0 讨论(0)
  • 2020-11-22 10:09

    The really tricky part here was killing the long running task through passing the executor thread from the Action back to a place where it could be aborted. I accomplished this with the use of a wrapped delegate that passes out the thread to kill into a local variable in the method that created the lambda.

    I submit this example, for your enjoyment. The method you are really interested in is CallWithTimeout. This will cancel the long running thread by aborting it, and swallowing the ThreadAbortException:

    Usage:

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

    The static method doing the work:

        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();
            }
        }
    
    }
    
    0 讨论(0)
  • 2020-11-22 10:23

    We are using code like this heavily in production:

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

    Implementation is open-sourced, works efficiently even in parallel computing scenarios and is available as a part of 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);
        }
    }
    

    This code is still buggy, you can try with this small test program:

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

    There is a race condition. It is clearly possible that a ThreadAbortException gets raised after the method WaitFor<int>.Run() is being called. I didn't find a reliable way to fix this, however with the same test I cannot repro any problem with the TheSoftwareJedi accepted answer.

    0 讨论(0)
  • 2020-11-22 10:23

    Some minor changes to Pop Catalin's great answer:

    • Func instead of Action
    • Throw exception on bad timeout value
    • Calling EndInvoke in case of timeout

    Overloads have been added to support signaling worker to cancel execution:

    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
    }
    
    0 讨论(0)
  • 2020-11-22 10:23

    I just knocked this out now so it might need some improvement, but will do what you want. It is a simple console app, but demonstrates the principles needed.

    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.");
            }
        }
    
    }
    
    0 讨论(0)
提交回复
热议问题