Espere asíncronamente para que la Tarea se complete con tiempo de espera

Quiero esperar a que se complete una Tarea con algunas reglas especiales: si no se ha completado después de X milisegundos, quiero mostrar un mensaje al usuario. Y si no se completó después de Y milisegundos, deseo solicitar la cancelación automáticamente.

Puedo usar Task.ContinueWith para esperar asíncronamente a que la tarea se complete (es decir, progtwigr una acción para que se ejecute cuando la tarea se complete), pero eso no permite especificar un tiempo de espera excedido. Puedo usar Task.Wait para esperar sincrónicamente a que la tarea se complete con un tiempo de espera, pero eso bloquea mi hilo. ¿Cómo puedo esperar asíncronamente para que la tarea se complete con un tiempo de espera excedido?

Qué tal esto:

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

Y aquí hay una gran publicación en el blog “Elaboración de una tarea. Método TimeTafterout” (del equipo de MS Parallel Library) con más información sobre este tipo de cosas .

Además : a pedido de un comentario sobre mi respuesta, aquí hay una solución ampliada que incluye el manejo de la cancelación. Tenga en cuenta que la cancelación de la tarea y el temporizador significa que hay varias formas en que se puede experimentar la cancelación en su código, y debe asegurarse de probar y estar seguro de que maneja adecuadamente todas ellas. No deje de lado varias combinaciones y espere que su computadora haga lo correcto en tiempo de ejecución.

 int timeout = 1000; var task = SomeOperationAsync(cancellationToken); if (await Task.WhenAny(task, Task.Delay(timeout, cancellationToken)) == task) { // Task completed within timeout. // Consider that the task may have faulted or been canceled. // We re-await the task so that any exceptions/cancellation is rethrown. await task; } else { // timeout/cancellation logic } 

Aquí hay una versión del método de extensión que incorpora la cancelación del tiempo de espera cuando la tarea original finaliza según lo sugerido por Andrew Arnott en un comentario a su respuesta .

 public static async Task TimeoutAfter(this Task 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."); } } } 

Puede usar Task.WaitAny para esperar la primera de varias tareas.

Puede crear dos tareas adicionales (que finalicen después de los tiempos de espera especificados) y luego usar WaitAny para esperar lo que ocurra primero. Si la tarea que completó primero es su tarea de “trabajo”, entonces ha terminado. Si la tarea que se completó primero es una tarea de tiempo de espera, entonces puede reactjsr al tiempo de espera (por ejemplo, cancelación de solicitud).

¿Qué tal algo así?

  const int x = 3000; const int y = 1000; static void Main(string[] args) { // Your scheduler TaskScheduler scheduler = TaskScheduler.Default; Task nonblockingTask = new Task(() => { CancellationTokenSource source = new CancellationTokenSource(); Task t1 = new Task(() => { while (true) { // Do something if (source.IsCancellationRequested) break; } }, source.Token); t1.Start(scheduler); // Wait for task 1 bool firstTimeout = t1.Wait(x); if (!firstTimeout) { // If it hasn't finished at first timeout display message Console.WriteLine("Message to user: the operation hasn't completed yet."); bool secondTimeout = t1.Wait(y); if (!secondTimeout) { source.Cancel(); Console.WriteLine("Operation stopped!"); } } }); nonblockingTask.Start(); Console.WriteLine("Do whatever you want..."); Console.ReadLine(); } 

Puede usar la opción Task.Wait sin bloquear el hilo principal usando otra Tarea.

Aquí hay un ejemplo completamente trabajado basado en la respuesta más votado, que es:

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

La principal ventaja de la implementación en esta respuesta es que se han agregado generics, por lo que la función (o tarea) puede devolver un valor. Esto significa que cualquier función existente se puede envolver en una función de tiempo de espera, por ejemplo:

Antes de:

 int x = MyFunc(); 

Después:

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

Este código requiere .NET 4.5.

 using System; using System.Threading; using System.Threading.Tasks; namespace TaskTimeout { public static class Program { ///  /// Demo of how to wrap any function in a timeout. ///  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( this Func func, TimeSpan timeout) { var task = Task.Run(func); return TimeoutAfterAsync(task, timeout).GetAwaiter().GetResult(); } private static async Task TimeoutAfterAsync( this Task 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(); } } } } 

Advertencias

Habiendo dado esta respuesta, generalmente no es una buena práctica tener excepciones arrojadas en su código durante el funcionamiento normal, a menos que tenga que:

  • Cada vez que se lanza una excepción, es una operación extremadamente pesada,
  • Las excepciones pueden reducir la velocidad de su código en un factor de 100 o más si las excepciones están en un circuito cerrado.

Solo use este código si no puede alterar la función a la que está llamando por lo que agota el tiempo después de un TimeSpan específico.

Esta respuesta solo es aplicable cuando se trata de bibliotecas de bibliotecas de terceros que simplemente no puede refactorizar para incluir un parámetro de tiempo de espera excedido.

Cómo escribir código robusto

Si desea escribir un código robusto, la regla general es esta:

Cada operación individual que potencialmente podría bloquear indefinidamente, debe tener un tiempo de espera.

Si no observa esta regla, su código eventualmente golpeará una operación que falla por algún motivo, luego se bloqueará indefinidamente y su aplicación se colgará de manera permanente.

Si hubo un tiempo de espera razonable después de un tiempo, su aplicación se bloqueó durante un tiempo extremo (por ejemplo, 30 segundos) y luego mostraría un error y continuaría de manera feliz, o lo volvería a intentar.

Use un temporizador para manejar el mensaje y la cancelación automática. Cuando la tarea se complete, llame a Dispose en los temporizadores para que nunca se disparen. Aquí hay un ejemplo; cambie taskDelay a 500, 1500 o 2500 para ver los diferentes casos:

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

Además, Async CTP proporciona un método TaskEx.Delay que ajustará los temporizadores en tareas para usted. Esto puede darle más control para hacer cosas como configurar TaskScheduler para la continuación cuando se dispara el temporizador.

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

Otra forma de resolver este problema es usar extensiones reactivas:

 public static Task TimeoutAfter(this Task task, TimeSpan timeout, IScheduler scheduler) { return task.ToObservable().Timeout(timeout, scheduler).ToTask(); } 

Prueba arriba usando el código de abajo en tu prueba de unidad, funciona para mí

 TestScheduler scheduler = new TestScheduler(); Task task = Task.Run(() => { int i = 0; while (i < 5) { Console.WriteLine(i); i++; Thread.Sleep(1000); } }) .TimeoutAfter(TimeSpan.FromSeconds(5), scheduler) .ContinueWith(t => { }, TaskContinuationOptions.OnlyOnFaulted); scheduler.AdvanceBy(TimeSpan.FromSeconds(6).Ticks); 

Es posible que necesite el siguiente espacio de nombre:

 using System.Threading.Tasks; using System.Reactive.Subjects; using System.Reactive.Linq; using System.Reactive.Threading.Tasks; using Microsoft.Reactive.Testing; using System.Threading; using System.Reactive.Concurrency; 

Usando la excelente biblioteca AsyncEx de Stephen Cleary, puede hacer:

 TimeSpan timeout = TimeSpan.FromSeconds(10); using (var cts = new CancellationTokenSource(timeout)) { await myTask.WaitAsync(cts.Token); } 

TaskCanceledException se TaskCanceledException en caso de que se TaskCanceledException el tiempo de espera.

Una versión genérica de la respuesta de @ Kevan anterior con Reactive Extensions.

 public static Task TimeoutAfter(this Task task, TimeSpan timeout, IScheduler scheduler) { return task.ToObservable().Timeout(timeout, scheduler).ToTask(); } 

Con progtwigdor opcional:

 public static Task TimeoutAfter(this Task task, TimeSpan timeout, Scheduler scheduler = null) { return scheduler == null ? task.ToObservable().Timeout(timeout).ToTask() : task.ToObservable().Timeout(timeout, scheduler).ToTask(); } 

Por cierto: cuando se produce un tiempo de espera, se lanzará una excepción de tiempo de espera

Si utiliza BlockingCollection para progtwigr la tarea, el productor puede ejecutar la tarea potencialmente larga y el consumidor puede usar el método TryTake que tiene integrado token de tiempo de espera y cancelación.