Domanda

Voglio aspettare un Attività<T> per completare con alcune regole speciali:Se non è stata completata dopo X millisecondi, voglio mostrare un messaggio all'utente.E se non è stata completata dopo Y millisecondi, voglio automaticamente la richiesta di cancellazione.

Posso usare Attività.ContinueWith in modo asincrono attendere il completamento dell'attività (es.pianificazione di un'azione da eseguire quando l'operazione è terminata), ma che non permette di specificare un timeout.Posso usare Attività.Attendere per sincrono attendere il completamento dell'attività con un timeout, ma che blocca il mio thread.Come posso fare in modo asincrono attendere il completamento dell'attività con un timeout?

È stato utile?

Soluzione

Che ne dici 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 "Creazione di un'Attività.TimeoutAfter Metodo" (da MS Parallel Library team) con più info su questo genere di cose.

Oltre:alla richiesta di un commento sulla mia risposta, qui è una versione ampliata soluzione che include la cancellazione di gestione.Si noti che il passaggio di annullamento del compito e il timer significa che ci sono più modi di cancellazione può essere vissuta nel codice, e si dovrebbe essere sicuri di prova e per essere sicuri di gestire correttamente tutti loro.Non lasciare al caso le varie combinazioni e spero che il tuo computer fa la cosa giusta in fase di runtime.

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
}

Altri suggerimenti

Ecco una versione metodo di estensione che incorpora la cancellazione del timeout quando il completamento del compito originario come suggerito da Andrew Arnott in un commento alla la 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.");
        }
    }
}

È possibile utilizzare Task.WaitAny aspettare il primo di più attività.

È possibile creare due attività aggiuntive (che completa dopo i timeout specificati) e poi l'uso WaitAny aspettare per qualsiasi completa prima. Se l'attività che ha completato prima è il tuo compito "lavoro", allora il gioco è fatto. Se l'attività che ha completato prima è un compito timeout, allora si può reagire al timeout (ad esempio, richiesta di annullamento).

Che dire qualcosa di simile?

    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 thread principale utilizzando un altro compito.

Ecco un esempio completamente lavorato sulla base della parte superiore ha votato risposta, che è:

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

Il principale vantaggio della realizzazione in questa risposta è che sono stati aggiunti generici, per cui la funzione (o l'attività) possono restituire un valore. Questo significa che qualsiasi funzione esistente può essere avvolto in una funzione di timeout, per 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();
            }
        }
    }
}

Avvertimenti

Dopo aver dato questa risposta, il suo genere non una pratica bene avere le eccezioni sollevate nel codice durante il normale funzionamento, a meno che non dovete assolutamente:

  • Ogni volta che viene generata un'eccezione, il suo un funzionamento estremamente pesante,
  • Eccezioni possono rallentare il codice verso il basso di un fattore 100 o più se le eccezioni sono in un loop stretto.

utilizzare questo codice solo se assolutamente non si può alterare la funzione che si sta chiamando in modo che i tempi dopo una specifica TimeSpan.

Questa risposta è davvero applicabile soltanto quando si tratta di 3 ° librerie libreria partito che semplicemente non può refactoring di includere un parametro di timeout.

Come scrivere codice robusto

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

  

Ogni singola operazione che potrebbe potenzialmente bloccare a tempo indeterminato, deve avere un timeout.

Se non osservare questa regola, il tuo codice finirà per colpire un'operazione che non riesce per qualche motivo, allora sarà bloccare a tempo indeterminato, e la vostra applicazione ha appena appeso in modo permanente.

Se ci fosse un timeout ragionevole dopo qualche tempo, poi la vostra applicazione potrebbe appendere per una certa quantità estrema di tempo (ad esempio 30 secondi) allora dovrebbero o visualizzare un errore e continuare per la sua strada, o tentativo.

Usa un Timer per gestire il messaggio e cancellazione automatica. Quando il completamento del task, chiamare Dispose sui timer in modo che non sarà mai il fuoco. Ecco un esempio; cambiamento taskDelay a 500, 1500 o 2500 a 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 Async CTP fornisce un metodo che TaskEx.Delay avvolgerà i timer di compiti per voi. Questo può dare un maggiore controllo per fare le cose come impostare la TaskScheduler per la continuazione quando i fuochi 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;
}

Uso eccellente di Stephen Cleary AsyncEx biblioteca, si può fare:

TimeSpan timeout = TimeSpan.FromSeconds(10);

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

TaskCanceledException sarà gettato nel caso di un timeout.

Un altro modo per risolvere questo problema sta usando reattiva estensioni:

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

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

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:

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 versione generica di @ di Kevan risposta di cui sopra con reattivi estensioni.

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

Con opzionale Scheduler:

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

A proposito: Quando un timeout accade, verrà generata un'eccezione di timeout

Se si utilizza un BlockingCollection per pianificare l'attività, il produttore può eseguire l'attività potenzialmente lunga corsa e il consumatore può utilizzare il metodo TryTake che ha timeout e la cancellazione di token costruito in.

Questa è una versione leggermente migliorata del risposte precedenti.

  • In aggiunta a risposta di Lawrence, si annulla l'operazione originale quando si verifica il timeout.
  • In addtion a risposta di SJB varianti 2 e 3 , è possibile fornire CancellationToken per l'attività originaria, e quando si verifica il timeout , si ottiene TimeoutException invece 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, 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();
        }
    }
}

Alcuni varianti di risposta di Andrew Arnott:

  1. Se si desidera attendere un'attività esistente e scoprire se ha completato o scaduta, ma non si vuole annullare se il timeout si verifica:

    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 ed annullare il lavoro se il timeout si verifica:

    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 avete un compito già creato che si desidera annullare, se si verifica un 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 si annulla il timer se il timeout non si verifica, in modo più chiamate non causerà timer ad accumularsi.

SJB

ho sentito il compito Task.Delay() e CancellationTokenSource nelle altre risposte un po 'troppo per il mio caso d'uso in un ciclo di rete stretta-ish.

E anche se Joe Hoag di Crafting un metodo Task.TimeoutAfter sui blog MSDN è stata un'esperienza entusiasmante, ero un po 'stanco di utilizzare TimeoutException per il controllo di flusso per lo stesso motivo di cui sopra, perché timeout sono attesi più spesso di quanto non.

Così sono andato con questo, che gestisce anche le ottimizzazioni citati 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;
    }
}

caso Un esempio è l'uso in quanto tale:

var receivingTask = conn.ReceiveAsync(ct);

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

// Read and do something with data
var data = await receivingTask;
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top