Pregunta

quiero esperar a que un Tarea para completar 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 milisegundos Y, quiero automáticamente solicitud de cancelación .

Puedo usar Task.ContinueWith que esperar de forma asíncrona para que la tarea completa (es decir, horario una acción que se ejecutará cuando la tarea se ha completado), pero que no permite especificar un tiempo de espera. Puedo usar Task.Wait que esperar de forma sincrónica para la tarea de completar con un tiempo de espera, pero eso bloques mi hilo. ¿Cómo puedo asíncrona esperar a que se complete la tarea con un tiempo de espera?

¿Fue útil?

Solución

¿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 en el blog "La elaboración de un Grupo de .TimeoutAfter Método"(de MS paralelo equipo de la biblioteca) con más información sobre este tipo de cosas .

Adición : a petición de un comentario en mi respuesta, aquí es una solución ampliado que incluye el manejo de cancelación. Tenga en cuenta que la cancelación de pasar a la tarea y los medios temporizadores que hay múltiples formas de cancelación se puede experimentar en su código, y usted debe estar seguro de prueba para tener confianza y manejar adecuadamente todos ellos. No dejar al azar varias combinaciones y esperar su computadora hace 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
}

Otros consejos

Aquí hay una versión método de extensión que incorpora la cancelación del tiempo de espera cuando se complete la tarea originales según lo sugerido por 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.");
        }
    }
}

Se puede utilizar Task.WaitAny que esperar la primera de las múltiples tareas.

Se puede crear dos tareas adicionales (que se completa después de los tiempos de espera especificados) y luego el uso WaitAny que esperar a que sea completa la primera. Si la tarea que se ha completado la primera tarea es su "trabajo", entonces ya está. Si la tarea que se ha completado la primera es una tarea de tiempo de espera, entonces se puede reaccionar ante el tiempo de espera (por ejemplo, solicitar la cancelación).

¿Qué pasa 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();
    }

Se puede utilizar la opción Task.Wait sin bloquear el hilo principal con otra tarea.

Este es un ejemplo totalmente trabajado sobre la base de la parte superior votó respuesta, 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 aplicación de esta respuesta es que los genéricos se han añadido, por lo que la función (o tarea) puede devolver un valor. Esto significa que cualquier función existente puede ser envuelto 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

Después de haber dado esta respuesta, el general no una práctica bien tener excepciones producidas en el código durante el funcionamiento normal, a menos que sea absolutamente necesario:

  • Cada vez que se produce una excepción, su una operación extremadamente pesado,
  • Las excepciones puede ralentizar su código por un factor de 100 o más si las excepciones se encuentran en un bucle estrecho.

Sólo utilice este código si usted no puede alterar la función que está llamando por lo que el tiempo de espera después de un TimeSpan específica.

Esta respuesta es realmente sólo es aplicable cuando se trata de las bibliotecas de la biblioteca 3er partido que usted simplemente no puede perfeccionar por incluir un parámetro de tiempo de espera.

¿Cómo escribir código robusto

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

  

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

Si no con esta norma, el código será finalmente golpear una operación que falla por alguna razón, entonces se bloqueará de forma indefinida, y su aplicación ha colgado simplemente de forma permanente.

Si hubo un tiempo de espera razonable después de algún tiempo, a continuación, su aplicación sería colgar por alguna cantidad extrema de tiempo (por ejemplo, 30 segundos) entonces sería bien mostrará un error y continuar en su camino feliz, o reintento.

Utilice un temporizador para manejar el mensaje y la cancelación automática. Cuando se complete tareas, llame a Dispose en los temporizadores para que nunca se disparará. Aquí hay un ejemplo; cambio 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, el Async CTP proporciona un método TaskEx.Delay que envolverá los contadores de tiempo en tareas para usted. Esto le puede dar más control para hacer cosas como establecer el TaskScheduler para la continuación cuando se activa 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;
}

El uso excelente de Stephen Cleary AsyncEx biblioteca, que puede hacer:

TimeSpan timeout = TimeSpan.FromSeconds(10);

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

TaskCanceledException será lanzada en el caso de un tiempo de espera.

Otra forma de resolver este problema está utilizando Reactivo Extensiones:

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

Prueba arriba mediante el siguiente código en su prueba de unidad, que 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 espacio de nombres siguiente:

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;

Una versión genérica de 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 produce una excepción de tiempo de espera

Si utiliza un BlockingCollection para programar la tarea, el productor puede ejecutar la tarea potencialmente larga duración y el consumidor puede utilizar el método TryTake que tiene tiempo de espera y la cancelación de señal incorporado.

Esta es una versión ligeramente mejorada de las respuestas anteriores.

  • de Lawrence respuesta , se cancela la tarea original cuando se produce tiempo de espera.
  • de sjb respuesta variantes 2 y 3 , puede proporcionar CancellationToken para la tarea original, y cuando se produce tiempo de espera , se obtiene TimeoutException en lugar de 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, combinedCancellation.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();
        }
    }
}

Algunas variantes de la respuesta de Andrew Arnott:

  1. Si desea esperar a que una tarea existente y averiguar si se ha completado o tiempo de espera, pero no quiere cancelarla si se produce el tiempo de espera:

    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. Si desea iniciar una tarea de trabajo y cancelar el trabajo si se produce el tiempo de espera:

    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. Si usted tiene una tarea que ya ha creado desea cancelar si se produce un tiempo de espera:

    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
    }
    

Otro comentario, estas versiones, se cancelará el temporizador si el tiempo de espera no se produce, por lo que múltiples llamadas que no causa temporizadores a acumularse.

sjb

Me sentía la tarea Task.Delay() y CancellationTokenSource en las otras respuestas un poco demasiado para mi caso de uso en un bucle de red apretada-ish.

Joe Hoag de la elaboración de un método Task.TimeoutAfter en los blogs de MSDN fue inspirador, que estaba un poco cansado de usar TimeoutException para el control de flujo por la misma razón que el anterior, ya que los tiempos de espera se espera que con más frecuencia que no.

Así que fui con esto, que también se encarga de 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 del ejemplo, el uso 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;
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top