Asynchronously wait for Task to complete with timeout

后端 未结 16 2021
天命终不由人
天命终不由人 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:31

    This is a slightly enhanced version of previous answers.

    • In addition to Lawrence's answer, it cancels the original task when timeout occurs.
    • In addtion to sjb's answer variants 2 and 3, you can provide CancellationToken for the original task, and when timeout occurs, you get TimeoutException instead of OperationCanceledException.
    async Task<TResult> CancelAfterAsync<TResult>(
        Func<CancellationToken, Task<TResult>> startTask,
        TimeSpan timeout, CancellationToken cancellationToken)
    {
        using (var timeoutCancellation = new CancellationTokenSource())
        using (var combinedCancellation = CancellationTokenSource
            .CreateLinkedTokenSource(cancellationToken, timeoutCancellation.Token))
        {
            var originalTask = startTask(combinedCancellation.Token);
            var delayTask = Task.Delay(timeout, timeoutCancellation.Token);
            var completedTask = await Task.WhenAny(originalTask, delayTask);
            // Cancel timeout to stop either task:
            // - Either the original task completed, so we need to cancel the delay task.
            // - Or the timeout expired, so we need to cancel the original task.
            // Canceling will not affect a task, that is already completed.
            timeoutCancellation.Cancel();
            if (completedTask == originalTask)
            {
                // original task completed
                return await originalTask;
            }
            else
            {
                // timeout
                throw new TimeoutException();
            }
        }
    }
    

    Usage

    InnerCallAsync may take a long time to complete. CallAsync wraps it with a timeout.

    async Task<int> CallAsync(CancellationToken cancellationToken)
    {
        var timeout = TimeSpan.FromMinutes(1);
        int result = await CancelAfterAsync(ct => InnerCallAsync(ct), timeout,
            cancellationToken);
        return result;
    }
    
    async Task<int> InnerCallAsync(CancellationToken cancellationToken)
    {
        return 42;
    }
    
    0 讨论(0)
  • 2020-11-21 18:31

    Use a Timer to handle the message and automatic cancellation. When the Task completes, call Dispose on the timers so that they will never fire. Here is an example; change taskDelay to 500, 1500, or 2500 to see the different cases:

    using System;
    using System.Threading;
    using System.Threading.Tasks;
    
    namespace ConsoleApplication1
    {
        class Program
        {
            private static Task CreateTaskWithTimeout(
                int xDelay, int yDelay, int taskDelay)
            {
                var cts = new CancellationTokenSource();
                var token = cts.Token;
                var task = Task.Factory.StartNew(() =>
                {
                    // Do some work, but fail if cancellation was requested
                    token.WaitHandle.WaitOne(taskDelay);
                    token.ThrowIfCancellationRequested();
                    Console.WriteLine("Task complete");
                });
                var messageTimer = new Timer(state =>
                {
                    // Display message at first timeout
                    Console.WriteLine("X milliseconds elapsed");
                }, null, xDelay, -1);
                var cancelTimer = new Timer(state =>
                {
                    // Display message and cancel task at second timeout
                    Console.WriteLine("Y milliseconds elapsed");
                    cts.Cancel();
                }
                    , null, yDelay, -1);
                task.ContinueWith(t =>
                {
                    // Dispose the timers when the task completes
                    // This will prevent the message from being displayed
                    // if the task completes before the timeout
                    messageTimer.Dispose();
                    cancelTimer.Dispose();
                });
                return task;
            }
    
            static void Main(string[] args)
            {
                var task = CreateTaskWithTimeout(1000, 2000, 2500);
                // The task has been started and will display a message after
                // one timeout and then cancel itself after the second
                // You can add continuations to the task
                // or wait for the result as needed
                try
                {
                    task.Wait();
                    Console.WriteLine("Done waiting for task");
                }
                catch (AggregateException ex)
                {
                    Console.WriteLine("Error waiting for task:");
                    foreach (var e in ex.InnerExceptions)
                    {
                        Console.WriteLine(e);
                    }
                }
            }
        }
    }
    

    Also, the Async CTP provides a TaskEx.Delay method that will wrap the timers in tasks for you. This can give you more control to do things like set the TaskScheduler for the continuation when the Timer fires.

    private static Task CreateTaskWithTimeout(
        int xDelay, int yDelay, int taskDelay)
    {
        var cts = new CancellationTokenSource();
        var token = cts.Token;
        var task = Task.Factory.StartNew(() =>
        {
            // Do some work, but fail if cancellation was requested
            token.WaitHandle.WaitOne(taskDelay);
            token.ThrowIfCancellationRequested();
            Console.WriteLine("Task complete");
        });
    
        var timerCts = new CancellationTokenSource();
    
        var messageTask = TaskEx.Delay(xDelay, timerCts.Token);
        messageTask.ContinueWith(t =>
        {
            // Display message at first timeout
            Console.WriteLine("X milliseconds elapsed");
        }, TaskContinuationOptions.OnlyOnRanToCompletion);
    
        var cancelTask = TaskEx.Delay(yDelay, timerCts.Token);
        cancelTask.ContinueWith(t =>
        {
            // Display message and cancel task at second timeout
            Console.WriteLine("Y milliseconds elapsed");
            cts.Cancel();
        }, TaskContinuationOptions.OnlyOnRanToCompletion);
    
        task.ContinueWith(t =>
        {
            timerCts.Cancel();
        });
    
        return task;
    }
    
    0 讨论(0)
  • 2020-11-21 18:31

    Definitely don't do this, but it is an option if ... I can't think of a valid reason.

    ((CancellationTokenSource)cancellationToken.GetType().GetField("m_source",
        System.Reflection.BindingFlags.NonPublic |
        System.Reflection.BindingFlags.Instance
    ).GetValue(cancellationToken)).Cancel();
    
    0 讨论(0)
  • 2020-11-21 18:35

    Here's a extension method version that incorporates cancellation of the timeout when the original task completes as suggested by Andrew Arnott in a comment to his answer.

    public static async Task<TResult> TimeoutAfter<TResult>(this Task<TResult> task, TimeSpan timeout) {
    
        using (var timeoutCancellationTokenSource = new CancellationTokenSource()) {
    
            var completedTask = await Task.WhenAny(task, Task.Delay(timeout, timeoutCancellationTokenSource.Token));
            if (completedTask == task) {
                timeoutCancellationTokenSource.Cancel();
                return await task;  // Very important in order to propagate exceptions
            } else {
                throw new TimeoutException("The operation has timed out.");
            }
        }
    }
    
    0 讨论(0)
  • 2020-11-21 18:37

    You can use Task.WaitAny to wait the first of multiple tasks.

    You could create two additional tasks (that complete after the specified timeouts) and then use WaitAny to wait for whichever completes first. If the task that completed first is your "work" task, then you're done. If the task that completed first is a timeout task, then you can react to the timeout (e.g. request cancellation).

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

    A few variants of Andrew Arnott's answer:

    1. If you want to wait for an existing task and find out whether it completed or timed out, but don't want to cancel it if the timeout occurs:

      public static async Task<bool> TimedOutAsync(this Task task, int timeoutMilliseconds)
      {
          if (timeoutMilliseconds < 0 || (timeoutMilliseconds > 0 && timeoutMilliseconds < 100)) { throw new ArgumentOutOfRangeException(); }
      
          if (timeoutMilliseconds == 0) {
              return !task.IsCompleted; // timed out if not completed
          }
          var cts = new CancellationTokenSource();
          if (await Task.WhenAny( task, Task.Delay(timeoutMilliseconds, cts.Token)) == task) {
              cts.Cancel(); // task completed, get rid of timer
              await task; // test for exceptions or task cancellation
              return false; // did not timeout
          } else {
              return true; // did timeout
          }
      }
      
    2. If you want to start a work task and cancel the work if the timeout occurs:

      public static async Task<T> CancelAfterAsync<T>( this Func<CancellationToken,Task<T>> actionAsync, int timeoutMilliseconds)
      {
          if (timeoutMilliseconds < 0 || (timeoutMilliseconds > 0 && timeoutMilliseconds < 100)) { throw new ArgumentOutOfRangeException(); }
      
          var taskCts = new CancellationTokenSource();
          var timerCts = new CancellationTokenSource();
          Task<T> task = actionAsync(taskCts.Token);
          if (await Task.WhenAny(task, Task.Delay(timeoutMilliseconds, timerCts.Token)) == task) {
              timerCts.Cancel(); // task completed, get rid of timer
          } else {
              taskCts.Cancel(); // timer completed, get rid of task
          }
          return await task; // test for exceptions or task cancellation
      }
      
    3. If you have a task already created that you want to cancel if a timeout occurs:

      public static async Task<T> CancelAfterAsync<T>(this Task<T> task, int timeoutMilliseconds, CancellationTokenSource taskCts)
      {
          if (timeoutMilliseconds < 0 || (timeoutMilliseconds > 0 && timeoutMilliseconds < 100)) { throw new ArgumentOutOfRangeException(); }
      
          var timerCts = new CancellationTokenSource();
          if (await Task.WhenAny(task, Task.Delay(timeoutMilliseconds, timerCts.Token)) == task) {
              timerCts.Cancel(); // task completed, get rid of timer
          } else {
              taskCts.Cancel(); // timer completed, get rid of task
          }
          return await task; // test for exceptions or task cancellation
      }
      

    Another comment, these versions will cancel the timer if the timeout does not occur, so multiple calls will not cause timers to pile up.

    sjb

    0 讨论(0)
提交回复
热议问题