Attendere in modo asincrono il completamento del task <T> con timeout


388

Voglio aspettare che un'attività <T> venga completata con alcune regole speciali: se non è stata completata dopo X millisecondi, voglio visualizzare un messaggio per l'utente. E se non si è completato dopo Y millisecondi, desidero richiedere automaticamente la cancellazione .

Posso utilizzare Task.ContinueWith per attendere in modo asincrono il completamento dell'attività (ovvero pianificare un'azione da eseguire al termine dell'attività ), ma ciò non consente di specificare un timeout. Posso usare Task.Wait per attendere in modo sincrono che l'attività si completi con un timeout, ma questo blocca il mio thread. Come posso attendere in modo asincrono il completamento dell'attività con un timeout?


3
Hai ragione. Sono sorpreso che non preveda il timeout. Forse in .NET 5.0 ... Naturalmente possiamo incorporare il timeout nell'attività stessa, ma non va bene, cose del genere devono essere gratuite.
Aliostad,

4
Sebbene richiederebbe ancora la logica per il timeout a due livelli che descrivi, .NET 4.5 offre effettivamente un metodo semplice per la creazione di un timeout basato CancellationTokenSource. Sono disponibili due sovraccarichi al costruttore, uno che richiede un ritardo di millisecondo intero e uno che richiede un ritardo TimeSpan.
Patridge,

La sorgente completo semplice lib qui: stackoverflow.com/questions/11831844/...

qualche soluzione finale con il codice sorgente completo funzionante? esempio forse più complesso per errori di notifica in ogni thread e dopo WaitAll mostra un riepilogo?
Kiquenet,

Risposte:


566

Cosa ne pensi di questo:

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

Ed ecco un grande post sul blog "Crafting a Task.TimeoutAfter Method" (dal team di MS Parallel Library) con maggiori informazioni su questo genere di cose .

Aggiunta : su richiesta di un commento sulla mia risposta, ecco una soluzione estesa che include la gestione delle cancellazioni. Nota che passare l'annullamento all'attività e al timer significa che ci sono diversi modi in cui è possibile sperimentare l'annullamento nel tuo codice e dovresti essere sicuro di testare ed essere sicuro di gestirli correttamente. Non lasciare al caso varie combinazioni e spera che il tuo computer faccia la cosa giusta in fase di esecuzione.

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
}

86
Va notato che anche se Task.Delay può essere completato prima di un'attività di lunga durata, consentendo di gestire uno scenario di timeout, questo NON annulla l'attività di lunga durata stessa; WhenAny ti fa semplicemente sapere che una delle attività passate ad essa è stata completata. Dovrai implementare un CancelToken e annullare tu stesso l'attività di lunga durata.
Jeff Schumacher,

30
Si può anche notare che il Task.Delay attività è supportata da un timer di sistema che continuerà a essere monitorato fino alla scadenza del timeout indipendentemente da quanto tempo SomeOperationAsyncimpiega. Quindi, se questo frammento di codice complessivo viene eseguito molto in un ciclo ristretto, stai consumando risorse di sistema per i timer fino a quando non scadono tutti. Il modo per risolvere il problema sarebbe quello di avere un CancellationTokenpassaggio a Task.Delay(timeout, cancellationToken)cui si annulla quando si SomeOperationAsynccompleta per rilasciare la risorsa timer.
Andrew Arnott,

12
Il codice di cancellazione sta facendo troppo lavoro. Prova questo: int timeout = 1000; var cancellazioneTokenSource = nuova CancellazioneTokenSource (timeout); var cancellazioneToken = tokenSource.Token; var task = SomeOperationAsync (notificationToken); provare {wait task; // Aggiungi qui il codice per il completamento riuscito} catch (OperationCancelledException) {// Aggiungi il codice qui per il caso di timeout}
srm

3
@ilans in attesa del Task, qualsiasi eccezione memorizzata dall'attività viene riproposta a quel punto. Questo ti dà la possibilità di catturare OperationCanceledException(se cancellato) o qualsiasi altra eccezione (se difettosa).
Andrew Arnott,

3
@TomexOu: la domanda era come attendere in modo asincrono il completamento di un'attività. Task.Wait(timeout)si bloccherebbe in modo sincrono invece di attendere in modo asincrono.
Andrew Arnott,

221

Ecco una versione del metodo di estensione che incorpora la cancellazione del timeout quando l'attività originale viene completata come suggerito da Andrew Arnott in un commento alla sua risposta .

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

8
Dai a quest'uomo dei voti. Soluzione elegante. E se la tua chiamata non ha un tipo di ritorno, assicurati di rimuovere il TResult.
Lucas,

6
CancellazioneTokenSource è usa e getta e dovrebbe essere in un usingblocco
PeterM

6
@ It'satrap In attesa di un'attività due volte restituisce semplicemente il risultato alla seconda attesa. Non viene eseguito due volte. Si potrebbe dire che è uguale task.Result quando eseguito due volte.
M. Mimpen

7
L'attività originale ( task) continuerà comunque a essere eseguita in caso di timeout?
jag

6
Minima opportunità di miglioramento: TimeoutExceptionpresenta un messaggio predefinito adeguato. Sostituendolo con "L'operazione è scaduta". non aggiunge valore e in realtà provoca un po 'di confusione implicando che c'è un motivo per sovrascriverlo.
Edward Brey,

49

È possibile utilizzare Task.WaitAnyper attendere la prima di più attività.

È possibile creare due attività aggiuntive (che si completano dopo i timeout specificati) e quindi utilizzare WaitAny per attendere quello che completa per primo. Se l'attività che è stata completata per prima è l'attività "lavoro", allora hai finito. Se l'attività che è stata completata per prima è un'attività di timeout, è possibile reagire al timeout (ad es. Richiesta di annullamento).


1
Ho visto questa tecnica usata da un MVP che rispetto davvero, mi sembra molto più pulita della risposta accettata. Forse un esempio aiuterebbe a ottenere più voti! Mi farei volontario per farlo, tranne per il fatto che non ho abbastanza esperienza Compito per essere sicuro che sarebbe utile :)
GrahamMc,

3
un thread verrebbe bloccato - ma se stai bene con quello, allora nessun problema. La soluzione che ho preso è stata la seguente, poiché nessun thread è bloccato. Ho letto il post sul blog che è stato davvero buono.
JJschk,

@JJschk hai detto che hai preso la soluzione below.... che cos'è? basato su ordini SO?
BozoJoe,

e se non desidero annullare l'attività più lenta? Voglio gestirlo quando finisce ma ritorna dal metodo attuale ..
Akmal Salikhov il

18

Che ne dici di qualcosa del genere?

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

È possibile utilizzare l'opzione Task.Wait senza bloccare il thread principale utilizzando un'altra attività.


In effetti in questo esempio non stai aspettando all'interno di t1 ma su un'attività superiore. Proverò a fare un esempio più dettagliato.
as-cii,

14

Ecco un esempio completo basato sulla risposta più votata, che è:

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

Il vantaggio principale dell'implementazione in questa risposta è che i generici sono stati aggiunti, quindi la funzione (o attività) può restituire un valore. Ciò significa che qualsiasi funzione esistente può essere racchiusa in una funzione di timeout, ad esempio:

Prima:

int x = MyFunc();

Dopo:

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

Questo codice richiede .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();
            }
        }
    }
}

Avvertenze

Dopo aver dato questa risposta, in genere non è una buona prassi avere eccezioni nel codice durante il normale funzionamento, a meno che non sia necessario:

  • Ogni volta che viene generata un'eccezione, è un'operazione estremamente pesante,
  • Le eccezioni possono rallentare il codice di un fattore pari o superiore a 100 se le eccezioni sono in un ciclo stretto.

Usa questo codice solo se non puoi assolutamente modificare la funzione che stai chiamando in modo che scada dopo uno specifico TimeSpan .

Questa risposta è davvero applicabile solo quando si ha a che fare con librerie di librerie di terze parti che semplicemente non è possibile includere in un parametro di timeout.

Come scrivere codice robusto

Se si desidera scrivere un codice affidabile, la regola generale è questa:

Ogni singola operazione che potrebbe potenzialmente bloccarsi indefinitamente, deve avere un timeout.

Se non lo fai questa regola, il tuo codice finirà per eseguire un'operazione che non riesce per qualche motivo, quindi si bloccherà indefinitamente e la tua app si è bloccata in modo permanente.

Se dopo un certo periodo di tempo si verificasse un timeout ragionevole, la tua app si bloccherebbe per un periodo di tempo estremo (ad es. 30 secondi), quindi visualizzerebbe un errore e continuerà nel modo giusto o riprova.


11

Utilizzando l'eccellente libreria AsyncEx di Stephen Cleary , puoi fare:

TimeSpan timeout = TimeSpan.FromSeconds(10);

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

TaskCanceledException verrà lanciato in caso di timeout.


10

Questa è una versione leggermente migliorata delle risposte precedenti.

  • Oltre alla risposta di Lawrence , annulla l'attività originale quando si verifica il timeout.
  • Oltre alle varianti di risposta 2 e 3 di sjb , è possibile fornire CancellationTokenl'attività originale e quando si verifica il timeout, si ottiene TimeoutExceptioninvece di 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, 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

InnerCallAsyncpotrebbe richiedere molto tempo per il completamento. CallAsynclo avvolge con un timeout.

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

1
Grazie per la soluzione! Sembra che si dovrebbe passare timeoutCancellationin delayTask. Attualmente, se si spegne l'annullamento, si CancelAfterAsyncpuò lanciare TimeoutExceptioninvece di TaskCanceledException, perché la causa delayTaskpotrebbe finire per prima.
AxelUser,

@AxelUser, hai ragione. Mi ci è voluta un'ora con un sacco di test unitari per capire cosa stava succedendo :) Ho pensato che quando entrambi i compiti assegnati fossero WhenAnyannullati dallo stesso token, WhenAnyrestituiremo il primo compito. Questa ipotesi era sbagliata. Ho modificato la risposta. Grazie!
Josef Bláha,

Sto facendo fatica a capire come chiamarlo effettivamente con una funzione Task <SomeResult> definita; qualche possibilità che potresti cogliere su un esempio di come chiamarlo?
jhaagsma,

1
@jhaagsma, esempio aggiunto!
Josef Bláha,

@ JosefBláha Grazie mille! Sto ancora lentamente avvolgendo la mia testa attorno alla sintassi in stile lambda, che non mi sarebbe venuta in mente - che il token viene passato all'attività nel corpo di CancelAfterAsync, passando la funzione lambda. Nifty!
jhaagsma,

8

Utilizzare un timer per gestire il messaggio e la cancellazione automatica. Al termine dell'attività, chiamare Dispose sui timer in modo che non vengano mai attivati. Ecco un esempio; modifica attività Ritardare a 500, 1500 o 2500 per vedere i diversi casi:

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

Inoltre, il CTP Async fornisce un metodo TaskEx.Delay che avvolge i timer nelle attività per te. Questo può darti un maggiore controllo per fare cose come impostare TaskScheduler per la continuazione quando si attiva il timer.

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

Non vuole che il thread corrente sia bloccato, cioè no task.Wait().
Cheng Chen,

@Danny: era solo per completare l'esempio. Dopo ContinueWith è possibile tornare e consentire l'esecuzione dell'attività. Aggiornerò la mia risposta per renderlo più chiaro.
Quartermeister,

2
@dtb: Cosa succede se si imposta t1 un'attività <Attività <Risult>> e quindi si chiama TaskExtensions.Unwrap? Puoi restituire t2 dal tuo lambda interno e in seguito puoi aggiungere continuazioni al compito da scartare.
Quartermeister,

Eccezionale! Questo risolve perfettamente il mio problema. Grazie! Penso che andrò con la soluzione proposta da @ AS-CII, anche se vorrei poter accettare la tua risposta anche per suggerire TaskExtensions.Unwrap Devo aprire una nuova domanda in modo da poter ottenere il rappresentante che meriti?
dtb,

6

Un altro modo di risolvere questo problema è usare le estensioni reattive:

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

Prova sopra usando il codice sotto nel tuo test unit, funziona per me

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

Potrebbe essere necessario il seguente spazio dei nomi:

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;

4

Una versione generica della risposta di @ Kevan sopra, usando le estensioni reattive.

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

Con Scheduler opzionale:

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

A proposito: quando si verifica un timeout, verrà generata un'eccezione di timeout


0

Se si utilizza BlockingCollection per pianificare l'attività, il produttore può eseguire l'attività potenzialmente in esecuzione a lungo termine e il consumatore può utilizzare il metodo TryTake con token di timeout e annullamento integrati.


Dovrei scrivere qualcosa (non voglio mettere qui il codice proprietario) ma lo scenario è così. Il produttore sarà il codice che esegue il metodo che potrebbe andare in timeout e, una volta terminato, inserirà i risultati nella coda. Il consumatore chiamerà trytake () con timeout e riceverà il token al timeout. Sia il produttore che il consumatore saranno attività di backround e visualizzeranno un messaggio all'utente utilizzando il dispatcher di thread dell'interfaccia utente, se necessario.
kns98,

0

Ho sentito il Task.Delay()compito e CancellationTokenSourcenell'altra risposta un po 'troppo per il mio caso d'uso in un circuito di rete molto stretto.

E sebbene la creazione di un'attività da parte di Joe Hoag. Il metodo TimeoutAfter sui blog MSDN era stimolante, ero un po 'stanco di utilizzare TimeoutExceptionper il controllo del flusso per lo stesso motivo di cui sopra, perché i timeout sono attesi più frequentemente.

Quindi sono andato con questo, che gestisce anche le ottimizzazioni menzionate nel 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 esempio di utilizzo è il seguente:

var receivingTask = conn.ReceiveAsync(ct);

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

// Read and do something with data
var data = await receivingTask;

0

Alcune varianti della risposta di Andrew Arnott:

  1. Se si desidera attendere un'attività esistente e scoprire se è stata completata o scaduta, ma non si desidera annullarla in caso di timeout:

    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. Se si desidera avviare un'attività di lavoro e annullare il lavoro in caso di timeout:

    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. Se hai già creato un'attività che desideri annullare in caso di timeout:

    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
    }

Un altro commento, queste versioni annulleranno il timer se non si verifica il timeout, quindi più chiamate non causeranno l'accumulo di timer.

SJB


0

Sto ricombinando qui le idee di alcune altre risposte e questa risposta su un altro thread in un metodo di estensione in stile Try. Questo ha un vantaggio se si desidera un metodo di estensione, evitando comunque un'eccezione al timeout.

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

-3

Sicuramente non farlo, ma è un'opzione se ... Non riesco a pensare a un motivo valido.

((CancellationTokenSource)cancellationToken.GetType().GetField("m_source",
    System.Reflection.BindingFlags.NonPublic |
    System.Reflection.BindingFlags.Instance
).GetValue(cancellationToken)).Cancel();
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.