Espera asíncronamente a que la tarea se complete con tiempo de espera


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

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

Author: dtb, 2010-11-21

11 answers

¿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í está una gran entrada de blog "Elaboración de una tarea.TimeoutAfter Method " (del equipo de la Biblioteca Paralela de MS) con más información sobre este tipo de cosas.

Addition: a petición de un comentario sobre mi respuesta, aquí hay una solución ampliada que incluye el manejo de la cancelación. Tenga en cuenta que pasar la cancelación a la tarea y el temporizador significa que hay múltiples formas en que se puede experimentar la cancelación en su código, y debe asegurarse de probar para y estar seguro de que manejar adecuadamente todos ellos. No deje al azar 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
}
 428
Author: Andrew Arnott,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-08-19 05:30:21

Aquí hay una versión del método de extensión que incorpora la cancelación del tiempo de espera cuando se completa la tarea original como lo sugirió Andrew Arnott en un comentario a su respuesta.

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.");
        }
    }
}
 138
Author: Lawrence Johnston,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-06-22 22:07:03

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

Puede crear dos tareas adicionales (que se completen después de los tiempos de espera especificados) y luego usar WaitAny para esperar a que se complete primero. Si la tarea que completaste primero es tu tarea de "trabajo", entonces has terminado. Si la tarea que se completó primero es una tarea de tiempo de espera, entonces puede reaccionar al tiempo de espera (por ejemplo, solicitar cancelación).

 40
Author: Tomas Petricek,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-07-19 01:15:38

¿Qué hay de algo como esto?

    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 Tarea.Esperar opción sin bloquear el hilo principal usando otra tarea.

 17
Author: as-cii,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-11-21 15:24:06

Aquí hay un ejemplo completamente trabajado basado en la respuesta más votada, 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 genéricos, por lo que la función (o tarea) puede devolver un valor. Esto significa que cualquier función existente puede ser envuelta en una función de tiempo de espera, por ejemplo:

Antes:

int x = MyFunc();

Después de:

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

Advertencias

Habiendo dado esto respuesta, generalmente no es una buena práctica tener excepciones lanzadas en su código durante el funcionamiento normal, a menos que sea absolutamente necesario:

  • Cada vez que se lanza una excepción, es una operación extremadamente pesada, {[29]]}
  • Las excepciones pueden ralentizar su código en un factor de 100 o más si las excepciones están en un bucle apretado.

Solo use este código si no puede alterar la función que está llamando para que se agote el tiempo después de un TimeSpan.

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

Cómo escribir código robusto

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

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

Si no observa esta regla, su código eventualmente llegará a una operación eso falla por alguna razón, luego se bloqueará indefinidamente y su aplicación se colgará permanentemente.

Si hubiera un tiempo de espera razonable después de algún tiempo, entonces su aplicación se colgaría durante una cantidad extrema de tiempo (por ejemplo, 30 segundos), entonces mostraría un error y continuaría en su camino feliz, o volvería a intentarlo.

 12
Author: Contango,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-06-30 16:33:14

Use un temporizador para manejar el mensaje y la cancelación automática. Cuando finalice la Tarea, llame a Dispose en los temporizadores para que nunca 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);
                }
            }
        }
    }
}

También, el Async CTP proporciona un TaskEx.Método de retardo que envolverá los temporizadores en tareas para usted. Esto puede darle más control para hacer cosas como configurar el 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;
}
 8
Author: Quartermeister,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-11-21 15:40:50

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 siguiente código en su prueba unitaria, 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);

Puede necesitar el siguiente espacio de nombres:

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;
 6
Author: Kevan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-06-17 02:33:31

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

TimeSpan timeout = TimeSpan.FromSeconds(10);

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

TaskCanceledException se lanzará en caso de un tiempo de espera.

 5
Author: Cocowalla,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-09-25 10:05:28

Una versión genérica de la respuesta de @Kevan anterior con Extensiones reactivas.

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

Con Programador opcional:

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

Por cierto: Cuando ocurre un tiempo de espera, se lanzará una excepción de tiempo de espera

 2
Author: Jasper H Bojsen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-18 16:13:44

Sentí la tarea Task.Delay() y CancellationTokenSource en las otras respuestas un poco demasiado para mi caso de uso en un bucle de red estrecho.

Y aunque Joe Hoag está Elaborando una Tarea.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.

Así que me fui con esto, que también maneja las optimizaciones mencionadas en el 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;
    }
}

Un caso de uso de ejemplo es como tal:

var receivingTask = conn.ReceiveAsync(ct);

while (!await receivingTask.BeforeTimeout(keepAliveMilliseconds))
{
    // Send keep-alive
}

// Read and do something with data
var data = await receivingTask;
 0
Author: antak,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-09-14 03:00:41

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

 -1
Author: kns98,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-02-12 15:10:15