Asynchronously wait for Task to complete with timeout

后端 未结 16 2003
天命终不由人
天命终不由人 2020-11-21 17:49

I want to wait for a Task to complete with some special rules: If it hasn\'t completed after X milliseconds, I want to display a message to the user. And

相关标签:
16条回答
  • 2020-11-21 18:40

    If you use a BlockingCollection to schedule the task, the producer can run the potentially long running task and the consumer can use the TryTake method which has timeout and cancellation token built in.

    0 讨论(0)
  • 2020-11-21 18:45

    Here is a fully worked example based on the top voted answer, which is:

    int timeout = 1000;
    var task = SomeOperationAsync();
    if (await Task.WhenAny(task, Task.Delay(timeout)) == task) {
        // task completed within timeout
    } else { 
        // timeout logic
    }
    

    The main advantage of the implementation in this answer is that generics have been added, so the function (or task) can return a value. This means that any existing function can be wrapped in a timeout function, e.g.:

    Before:

    int x = MyFunc();
    

    After:

    // Throws a TimeoutException if MyFunc takes more than 1 second
    int x = TimeoutAfter(MyFunc, TimeSpan.FromSeconds(1));
    

    This code requires .NET 4.5.

    using System;
    using System.Threading;
    using System.Threading.Tasks;
    
    namespace TaskTimeout
    {
        public static class Program
        {
            /// <summary>
            ///     Demo of how to wrap any function in a timeout.
            /// </summary>
            private static void Main(string[] args)
            {
    
                // Version without timeout.
                int a = MyFunc();
                Console.Write("Result: {0}\n", a);
                // Version with timeout.
                int b = TimeoutAfter(() => { return MyFunc(); },TimeSpan.FromSeconds(1));
                Console.Write("Result: {0}\n", b);
                // Version with timeout (short version that uses method groups). 
                int c = TimeoutAfter(MyFunc, TimeSpan.FromSeconds(1));
                Console.Write("Result: {0}\n", c);
    
                // Version that lets you see what happens when a timeout occurs.
                try
                {               
                    int d = TimeoutAfter(
                        () =>
                        {
                            Thread.Sleep(TimeSpan.FromSeconds(123));
                            return 42;
                        },
                        TimeSpan.FromSeconds(1));
                    Console.Write("Result: {0}\n", d);
                }
                catch (TimeoutException e)
                {
                    Console.Write("Exception: {0}\n", e.Message);
                }
    
                // Version that works on tasks.
                var task = Task.Run(() =>
                {
                    Thread.Sleep(TimeSpan.FromSeconds(1));
                    return 42;
                });
    
                // To use async/await, add "await" and remove "GetAwaiter().GetResult()".
                var result = task.TimeoutAfterAsync(TimeSpan.FromSeconds(2)).
                               GetAwaiter().GetResult();
    
                Console.Write("Result: {0}\n", result);
    
                Console.Write("[any key to exit]");
                Console.ReadKey();
            }
    
            public static int MyFunc()
            {
                return 42;
            }
    
            public static TResult TimeoutAfter<TResult>(
                this Func<TResult> func, TimeSpan timeout)
            {
                var task = Task.Run(func);
                return TimeoutAfterAsync(task, timeout).GetAwaiter().GetResult();
            }
    
            private static async Task<TResult> TimeoutAfterAsync<TResult>(
                this Task<TResult> task, TimeSpan timeout)
            {
                var result = await Task.WhenAny(task, Task.Delay(timeout));
                if (result == task)
                {
                    // Task completed within timeout.
                    return task.GetAwaiter().GetResult();
                }
                else
                {
                    // Task timed out.
                    throw new TimeoutException();
                }
            }
        }
    }
    

    Caveats

    Having given this answer, its generally not a good practice to have exceptions thrown in your code during normal operation, unless you absolutely have to:

    • Each time an exception is thrown, its an extremely heavyweight operation,
    • Exceptions can slow your code down by a factor of 100 or more if the exceptions are in a tight loop.

    Only use this code if you absolutely cannot alter the function you are calling so it times out after a specific TimeSpan.

    This answer is really only applicable when dealing with 3rd party library libraries that you simply cannot refactor to include a timeout parameter.

    How to write robust code

    If you want to write robust code, the general rule is this:

    Every single operation that could potentially block indefinitely, must have a timeout.

    If you do not observe this rule, your code will eventually hit an operation that fails for some reason, then it will block indefinitely, and your app has just permanently hung.

    If there was a reasonable timeout after some time, then your app would hang for some extreme amount of time (e.g. 30 seconds) then it would either display an error and continue on its merry way, or retry.

    0 讨论(0)
  • 2020-11-21 18:45

    I felt the Task.Delay() task and CancellationTokenSource in the other answers a bit much for my use case in a tight-ish networking loop.

    And although Joe Hoag's Crafting a Task.TimeoutAfter Method on MSDN blogs was inspiring, I was a little weary of using TimeoutException for flow control for the same reason as above, because timeouts are expected more frequently than not.

    So I went with this, which also handles the optimizations mentioned in the blog:

    public static async Task<bool> BeforeTimeout(this Task task, int millisecondsTimeout)
    {
        if (task.IsCompleted) return true;
        if (millisecondsTimeout == 0) return false;
    
        if (millisecondsTimeout == Timeout.Infinite)
        {
            await Task.WhenAll(task);
            return true;
        }
    
        var tcs = new TaskCompletionSource<object>();
    
        using (var timer = new Timer(state => ((TaskCompletionSource<object>)state).TrySetCanceled(), tcs,
            millisecondsTimeout, Timeout.Infinite))
        {
            return await Task.WhenAny(task, tcs.Task) == task;
        }
    }
    

    An example use case is as such:

    var receivingTask = conn.ReceiveAsync(ct);
    
    while (!await receivingTask.BeforeTimeout(keepAliveMilliseconds))
    {
        // Send keep-alive
    }
    
    // Read and do something with data
    var data = await receivingTask;
    
    0 讨论(0)
  • 2020-11-21 18:45

    I'm recombinging the ideas of some other answers here and this answer on another thread into a Try-style extension method. This has a benefit if you want an extension method, yet avoiding an exception upon timeout.

    public static async Task<bool> TryWithTimeoutAfter<TResult>(this Task<TResult> task,
        TimeSpan timeout, Action<TResult> successor)
    {
    
        using var timeoutCancellationTokenSource = new CancellationTokenSource();
        var completedTask = await Task.WhenAny(task, Task.Delay(timeout, timeoutCancellationTokenSource.Token))
                                      .ConfigureAwait(continueOnCapturedContext: false);
    
        if (completedTask == task)
        {
            timeoutCancellationTokenSource.Cancel();
    
            // propagate exception rather than AggregateException, if calling task.Result.
            var result = await task.ConfigureAwait(continueOnCapturedContext: false);
            successor(result);
            return true;
        }
        else return false;        
    }     
    
    async Task Example(Task<string> task)
    {
        string result = null;
        if (await task.TryWithTimeoutAfter(TimeSpan.FromSeconds(1), r => result = r))
        {
            Console.WriteLine(result);
        }
    }    
    
    0 讨论(0)
提交回复
热议问题