Espere asincrónicamente a que la tarea <T> se complete con tiempo de espera

388

Quiero esperar a que se complete una Tarea <T> 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 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 se complete la tarea), pero eso no permite especificar un tiempo de espera. 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 asincrónicamente a que la tarea se complete con un tiempo de espera?

dtb
fuente
3
Tienes razón. Me sorprende que no prevea el tiempo de espera. Tal vez en .NET 5.0 ... Por supuesto, podemos incorporar el tiempo de espera en la tarea en sí, pero eso no es bueno, tales cosas deben ser gratuitas.
Aliostad el
44
Si bien aún requeriría lógica para el tiempo de espera de dos niveles que describe, .NET 4.5 de hecho ofrece un método simple para crear un tiempo de espera basado CancellationTokenSource. Hay dos sobrecargas para el constructor, una con un retraso de milisegundos entero y otra con un retraso de TimeSpan.
patridge
La fuente completa de lib simple aquí: stackoverflow.com/questions/11831844/…
¿Alguna solución final con código fuente completo funcionando? ¿quizás una muestra más compleja para notificar errores en cada hilo y después de que WaitAll muestre un resumen?
Kiquenet

Respuestas:

566

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 de blog "Elaborando una tarea. Método Timeout After After" (del equipo de la Biblioteca paralela de MS) con más información sobre este tipo de cosas .

Además : 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 hay varias formas en que se puede experimentar la cancelación en su código, y debe asegurarse de probar y estar seguro de manejarlos adecuadamente. 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
}
Andrew Arnott
fuente
86
Cabe mencionar que aunque Task.Delay puede completarse antes de la tarea de ejecución larga, lo que le permite manejar un escenario de tiempo de espera, NO cancela la tarea de ejecución larga en sí; WhenAny simplemente le permite saber que una de las tareas que se le han pasado se ha completado. Tendrá que implementar un CancellationToken y cancelar la tarea de ejecución larga usted mismo.
Jeff Schumacher el
30
También se debe tener en cuenta que la Task.Delaytarea está respaldada por un temporizador del sistema que continuará siendo rastreado hasta que expire el tiempo de espera, independientemente de cuánto tiempo SomeOperationAsynctarde. Entonces, si este fragmento de código general se ejecuta mucho en un ciclo cerrado, está consumiendo recursos del sistema para temporizadores hasta que se agote el tiempo de espera. La forma de solucionarlo sería tener un CancellationTokenpase Task.Delay(timeout, cancellationToken)que cancele cuando se SomeOperationAsynccomplete para liberar el recurso del temporizador.
Andrew Arnott
12
El código de cancelación está haciendo muuuucho trabajo. Pruebe esto: int timeout = 1000; var cancellationTokenSource = nuevo CancellationTokenSource (tiempo de espera); var cancellationToken = tokenSource.Token; var task = SomeOperationAsync (cancellationToken); intente {aguarde la tarea; // Agregue código aquí para la finalización exitosa} catch (OperationCancelledException) {// Agregue código aquí para el caso de tiempo de espera}
srm
3
@ilans esperando el Task, cualquier excepción almacenada por la tarea se vuelve a lanzar en ese punto. Esto le da la oportunidad de atrapar OperationCanceledException(si se cancela) o cualquier otra excepción (si falla).
Andrew Arnott
3
@TomexOu: la pregunta era cómo esperar asincrónicamente la finalización de una tarea. Task.Wait(timeout)bloquearía sincrónicamente en lugar de esperar asincrónicamente.
Andrew Arnott
221

Aquí hay una versión del método de extensión que incorpora la cancelación del tiempo de espera cuando la tarea original se completa como lo sugiere 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.");
        }
    }
}
Lawrence Johnston
fuente
8
Dale a este hombre algunos votos. Solución elegante Y si su llamada no tiene un tipo de retorno, asegúrese de eliminar el TResult.
Lucas
66
CancellationTokenSource es desechable y debe estar en un usingbloque
PeterM
66
@ It'satrap En espera de una tarea dos veces, simplemente devuelve el resultado en la segunda espera. No se ejecuta dos veces. Se podría decir que es igual task.Result cuando se ejecuta dos veces.
M. Mimpen
77
¿Seguirá taskejecutándose la tarea original ( ) en caso de tiempo de espera?
jag
66
Oportunidad de mejora menor: TimeoutExceptiontiene un mensaje predeterminado adecuado. Al anularlo con "La operación ha excedido el tiempo de espera". no agrega valor y en realidad causa cierta confusión al implicar que hay una razón para anularlo.
Edward Brey
49

Puede usar Task.WaitAnypara esperar la primera de varias tareas.

Puede crear dos tareas adicionales (que se completan después de los tiempos de espera especificados) y luego usar WaitAnypara esperar lo que se complete primero. Si la tarea que se completó primero es su tarea de "trabajo", entonces ya está. Si la tarea que se completó primero es una tarea de tiempo de espera, entonces puede reaccionar al tiempo de espera (por ejemplo, solicitud de cancelación).

Tomás Petricek
fuente
1
He visto esta técnica utilizada por un MVP que realmente respeto, me parece mucho más limpia que la respuesta aceptada. ¡Quizás un ejemplo ayudaría a obtener más votos! Sería voluntario para hacerlo, excepto que no tengo suficiente experiencia en tareas para estar seguro de que sería útil :)
GrahamMc
3
un hilo estaría bloqueado, pero si estás de acuerdo con eso, entonces no hay problema. La solución que tomé fue la siguiente, ya que no hay hilos bloqueados. Leí la publicación del blog que fue realmente buena.
JJschk
@JJschk, mencionas que tomaste la solución below... ¿cuál es esa? basado en pedidos SO?
BozoJoe
¿Y si no quiero que se cancele la tarea más lenta? Quiero manejarlo cuando termine pero regreso del método actual ..
Akmal Salikhov
18

¿Qué tal 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.

as-cii
fuente
De hecho, en este ejemplo no está esperando dentro de t1 sino en una tarea superior. Trataré de hacer un ejemplo más detallado.
as-cii
14

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 incluir 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 la operación normal, a menos que 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 circuito cerrado.

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

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.

Cómo escribir código robusto

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

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

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

Si hubo un tiempo de espera razonable después de un tiempo, entonces su aplicación se colgaría durante un período de tiempo extremo (por ejemplo, 30 segundos) y luego mostraría un error y continuaría su camino feliz, o volvería a intentarlo.

Aplazamiento de pago
fuente
11

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 será lanzado en caso de un tiempo de espera.

Cocowalla
fuente
10

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

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

Uso

InnerCallAsyncpuede tardar mucho tiempo en completarse. CallAsynclo envuelve con un tiempo de espera.

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;
}
Josef Bláha
fuente
1
Gracias por la solucion! Parece que debe pasar timeoutCancellationen delayTask. Actualmente, si cancela la cancelación, CancelAfterAsyncpuede lanzar en TimeoutExceptionlugar de TaskCanceledException, la causa delayTaskpuede terminar primero.
AxelUser
@AxelUser, tienes razón. Me tomó una hora con un montón de pruebas unitarias para entender lo que estaba sucediendo :) Supuse que cuando las dos tareas asignadas WhenAnysean canceladas por el mismo token, WhenAnydevolverá la primera tarea. Esa suposición estaba mal. He editado la respuesta. ¡Gracias!
Josef Bláha
Me está costando mucho descubrir cómo llamar a esto realmente con una función definida Task <SomeResult>; ¿Hay alguna posibilidad de que puedas agregar un ejemplo de cómo llamarlo?
jhaagsma
1
@jhaagsma, ejemplo agregado!
Josef Bláha
@ JosefBláha Muchas gracias! Todavía estoy tratando de entender la sintaxis de estilo lambda, eso no se me habría ocurrido, que el token se pasa a la tarea en el cuerpo de CancelAfterAsync, pasando la función lambda. ¡Hábil!
jhaagsma
8

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 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, el CTP asíncrono proporciona un método TaskEx.Delay 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;
}
Quartermeister
fuente
No quiere que se bloquee el hilo actual, es decir, no task.Wait().
Cheng Chen el
@Danny: Eso fue solo para completar el ejemplo. Después de ContinueWith, puede regresar y dejar que la tarea se ejecute. Actualizaré mi respuesta para que quede más claro.
Quartermeister el
2
@dtb: ¿Qué sucede si convierte t1 en una Tarea <Tarea <Resultado>> y luego llama a TaskExtensions.Unwrap? Puede devolver t2 desde su lambda interior, y luego puede agregar continuaciones a la tarea sin envolver.
Quartermeister
¡Increíble! Eso resuelve perfectamente mi problema. ¡Gracias! Creo que iré con la solución propuesta por @ AS-CII, aunque desearía poder aceptar su respuesta también para sugerir Extensiones de tarea. Desenvolver ¿Debo abrir una nueva pregunta para que pueda obtener el representante que se merece?
dtb
6

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 la prueba de tu 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;
Kevan
fuente
4

Una versión genérica de la respuesta de @ Kevan anterior, usando 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 is null 
       ? task.ToObservable().Timeout(timeout).ToTask() 
       : task.ToObservable().Timeout(timeout, scheduler).ToTask();
}

Por cierto: cuando ocurre un tiempo de espera, se generará una excepción de tiempo de espera

Jasper H Bojsen
fuente
0

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.

kns98
fuente
Tendría que escribir algo (no quiero poner código propietario aquí) pero el escenario es así. El productor será el código que ejecuta el método que podría agotar el tiempo de espera y pondrá los resultados en la cola cuando termine. El consumidor llamará a trytake () con tiempo de espera y recibirá el token al finalizar el tiempo de espera. Tanto el productor como el consumidor serán tareas secundarias y mostrarán un mensaje al usuario utilizando el despachador de subprocesos de la interfaz de usuario si es necesario.
kns98
0

Sentí la Task.Delay()tarea y CancellationTokenSourceen las otras respuestas un poco demasiado para mi caso de uso en un circuito de red cerrado.

Y aunque Joe Hoag's Crafting a Task.TimeoutAfter Method en los blogs de MSDN fue inspirador, estaba un poco cansado de usar TimeoutExceptionel control de flujo por la misma razón que antes, porque los tiempos de espera se esperan con mayor frecuencia que no.

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;
antak
fuente
0

Algunas variantes de la respuesta de Andrew Arnott:

  1. Si desea esperar una tarea existente y averiguar si se completó o se agotó el tiempo de espera, pero no desea cancelarla si ocurre 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 ya ha creado una tarea que 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 cancelarán el temporizador si no se produce el tiempo de espera, por lo que varias llamadas no harán que los temporizadores se acumulen.

sjb

sjb-sjb
fuente
0

Estoy recombinando las ideas de algunas otras respuestas aquí y esta respuesta en otro hilo en un método de extensión al estilo Try. Esto tiene un beneficio si desea un método de extensión, pero evita una excepción en el tiempo de espera.

public static async Task<bool> TryWithTimeoutAfter<TResult>(this Task<TResult> task,
    TimeSpan timeout, Action<TResult> successor)
{

    using var timeoutCancellationTokenSource = new CancellationTokenSource();
    var completedTask = await Task.WhenAny(task, Task.Delay(timeout, timeoutCancellationTokenSource.Token))
                                  .ConfigureAwait(continueOnCapturedContext: false);

    if (completedTask == task)
    {
        timeoutCancellationTokenSource.Cancel();

        // propagate exception rather than AggregateException, if calling task.Result.
        var result = await task.ConfigureAwait(continueOnCapturedContext: false);
        successor(result);
        return true;
    }
    else return false;        
}     

async Task Example(Task<string> task)
{
    string result = null;
    if (await task.TryWithTimeoutAfter(TimeSpan.FromSeconds(1), r => result = r))
    {
        Console.WriteLine(result);
    }
}    
tm1
fuente
-3

Definitivamente no hagas esto, pero es una opción si ... No puedo pensar en una razón válida.

((CancellationTokenSource)cancellationToken.GetType().GetField("m_source",
    System.Reflection.BindingFlags.NonPublic |
    System.Reflection.BindingFlags.Instance
).GetValue(cancellationToken)).Cancel();
syko9000
fuente