c# como - Asincrónicamente, espere a que la tarea<T>se complete con el tiempo de espera




void net (10)

¿Qué pasa con 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 opción Task.Wait sin bloquear el hilo principal usando otra Tarea.

Quiero esperar a que una Task<T> 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, deseo solicitar automáticamente la cancelación .

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


Una versión genérica de @ Kevan's responde arriba 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 el 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 se produce un tiempo de espera, se lanzará una excepción de tiempo de espera


Si usa 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.


Sentí que la tarea Task.Delay() y el Task.Delay() en las otras respuestas son un poco demasiado para mi caso de uso en un bucle de redes muy estrecho.

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

Así que seguí 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 ejemplo de caso de 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;

Usando la excelente biblioteca AsyncEx Stephen Cleary, puedes hacer:

TimeSpan timeout = TimeSpan.FromSeconds(10);

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

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


Otra forma de resolver este problema es usando 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 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;

Utilice un Timer para manejar el mensaje y la cancelación automática. Cuando finalice la tarea, llame a Dispose en los temporizadores para que nunca se activen. 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, el CTP asíncrono proporciona un método TaskEx.Delay que envolverá los temporizadores en tareas para usted. Esto le puede dar 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;
}

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 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
    {
        /// <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 esta respuesta, generalmente no es una buena práctica tener excepciones en su código durante el funcionamiento normal, a menos que absolutamente tenga que:

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

Solo use este código si no puede alterar absolutamente la función a la que llama, por lo que se agota después de un TimeSpan específico.

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

Cómo escribir código robusto

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

Cada operación única que podría bloquearse indefinidamente, debe tener un tiempo de espera.

Si no cumple con esta regla, su código finalmente llegará a una operación que falla por alguna razón, luego se bloqueará indefinidamente y su aplicación se ha colgado permanentemente.

Si hubiera un tiempo de espera razonable después de algún tiempo, entonces su aplicación se colgaría durante un período de tiempo extremo (por ejemplo, 30 segundos), entonces mostraría un error y continuaría en su camino alegre o lo volvería a intentar.


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 excelente publicación en el blog "Crafting a Task.TimeoutAfter Method" (del equipo de MS Parallel Library) con más información sobre este tipo de cosas .

Adición : a solicitud 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 al temporizador significa que puede experimentar la cancelación de múltiples formas en su código, y debe asegurarse de probar y estar seguro de que los maneja correctamente. No deje al azar varias combinaciones y espere que su computadora haga lo correcto en el 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
}

Si tiene una licencia completa para Visual Studio y va a escribir solo programas de Windows solo para usted o para una empresa, entonces no habría ningún incentivo.

Sin embargo, si desea usar el lenguaje C # y el estilo .NET para un proyecto de código abierto, o uno que sea compatible en muchas plataformas, probablemente elija usar Mono lugar de .NET y SharpDevelop en lugar de VS.





c# .net task-parallel-library