2010-11-21 6 views
248

io   voglio aspettare per un Task<T> per completare con alcune regole speciali: Se non è completata dopo millisecondi X, io   desidera visualizzare un messaggio per l'utente. E se non è stato completato dopo Y millisecondi, I   desidera automaticamente request cancellation.asincrono attendere Task <T> completare con timeout

I   possibile utilizzare Task.ContinueWith attendere in modo asincrono per il compito di completare (cioè   pianificazione un'azione da eseguire quando l'attività è stata completata), ma che non permette di specificare un timeout. Posso usare Task.Wait per attendere in modo sincrono che l'attività si completi con un timeout, ma che blocchi il mio thread. Come posso attendere in modo asincrono il completamento dell'attività con un timeout?

+2

Hai ragione. Sono sorpreso che non preveda un timeout. Forse in .NET 5.0 ... Ovviamente possiamo costruire il timeout nel compito stesso, ma non va bene, queste cose devono venire gratis. – Aliostad

+3

Anche se richiederebbe una logica per il timeout a due livelli che si descrive, .NET 4.5 offre in effetti un metodo semplice per la creazione di un '' CancellationTokenSource' basato sul timeout (http://msdn.microsoft.com/en-us /library/system.threading.cancellationtokensource(v=VS.110).aspx). Sono disponibili due overload per il costruttore, uno con un ritardo di un millisecondo e uno con un ritardo di TimeSpan. – patridge

+0

Qui la completa fonte di lib libra: http://stackoverflow.com/questions/11831844/unobservedtaskexception-being-throw-but-it-is-handled-by-a-taskscheduler-unobser –

risposta

372

ne dite 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 a great blog post "Crafting a Task.TimeoutAfter Method" (from MS Parallel Library team) with more info on this sort of thing.

Aggiunta: su richiesta di un commento sulla mia risposta, ecco una soluzione ampliata che include la gestione della cancellazione. Nota che passare la cancellazione all'attività e al timer significa che ci sono più modi in cui la cancellazione può essere sperimentata nel tuo codice, e dovresti essere sicuro di testare e di essere sicuro di gestirli correttamente. Non lasciare al caso varie combinazioni e spero che il tuo computer faccia 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 
} 
+60

Va detto che anche se Task.Delay può completare prima dell'attività a lungo termine, consentendo di gestire uno scenario di timeout, questo NON annulla l'attività a lunga esecuzione stessa; Quando semplicemente ti fa sapere che una delle attività passate è terminata.Dovrai implementare un CancellationToken e annullare da solo l'attività di lunga durata. –

+18

Si può anche notare che il task 'Task.Delay' è supportato da un timer di sistema che continuerà a essere monitorato fino alla scadenza del timeout indipendentemente da quanto tempo' SomeOperationAsync' prende. Quindi, se questo snippet di codice complessivo viene eseguito molto in un ciclo limitato, stai consumando le risorse di sistema per i timer fino a quando non scadono tutti i timeout. Il modo per risolvere il problema sarebbe avere un 'CancellationToken' che passi a' Task.Delay (timeout, cancellationToken) 'che cancelli quando' SomeOperationAsync' completa per rilasciare la risorsa del timer. –

+4

Il codice di cancellazione sta facendo troppo lavoro. Prova questo: int timeout = 1000; var cancellationTokenSource = new CancellationTokenSource (timeout); var cancellationToken = tokenSource.Token; var task = SomeOperationAsync (cancellationToken); prova { attendere l'attività; // Aggiungere il codice qui per il buon esito } catch (OperationCancelledException) {// Aggiungere codice di qui per caso timeout } – srm

17

E 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 il thread principale utilizzando un'altra attività.

+0

Infatti in questo esempio non si sta aspettando all'interno di t1 ma su un'attività superiore. Proverò a fare un esempio più dettagliato. –

+0

Spero che questo esempio sia più chiaro per te. –

39

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

È possibile creare due attività aggiuntive (che si completano dopo i timeout specificati) e quindi utilizzare WaitAny per attendere prima che si completi. Se l'attività che è stata completata per prima è il tuo compito "lavoro", allora hai finito. Se l'attività 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 potrebbe aiutare ad ottenere più voti! Mi offrirò volontario per farlo, ma non ho abbastanza esperienza con il Task per essere sicuro che sarebbe utile :) – GrahamMc

+3

un thread sarebbe bloccato - ma se va bene allora non c'è problema. La soluzione che ho preso era quella sotto, poiché nessun thread è bloccato. Ho letto il post sul blog che è stato davvero buono. – JJschk

8

Utilizzare un Timer per gestire il messaggio e la cancellazione automatica. Al termine dell'attività, chiama Dispose sui timer in modo che non si attivino. Ecco un esempio; cambiare taskDelay a 500, 1500, 2500 o 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 Async CTP fornisce un metodo TaskEx.Delay che vi avvolgerà i timer di compiti per voi. Questo può darti un maggiore controllo per fare cose come impostare TaskScheduler per la continuazione quando il timer scocca.

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

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

+0

@Danny: Questo era solo per completare l'esempio. Dopo il ContinueWith è possibile tornare e lasciare eseguire l'attività. Aggiornerò la mia risposta per renderlo più chiaro. – Quartermeister

+0

Ho aggiornato la mia domanda. Potresti dare un'occhiata? – dtb

114

Ecco una versione metodo di estensione che incorpora la cancellazione del timeout quando il compito originale completa come suggerito da Andrew Arnott in un commento a his answer.

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

Sono in ritardo per la festa, ma l'attesa in tutte queste soluzioni non causa il blocco, rendendo così sincronizzata la chiamata asincrona? – Kir

+1

@ Kir No, non lo è, in realtà utilizzando Attendere con Task.WhenAny è ciò che consente * non * di bloccare (il controllo viene restituito al chiamante fino al completamento dell'attività atteso). Forse stai pensando a qualcosa come la soluzione di Tomas Petricek http://stackoverflow.com/a/4238575/1512 usando Task.WaitAny che blocca fino al completamento di una delle attività. –

+0

Ho avuto la possibilità di provarlo finalmente, e hai ragione! Sono un po 'confuso però - Sono arrugginito nella roba di Google. Perché funziona? TimeoutAfter ha un 'await' su Task.WhenAny; non dovrebbe bloccarlo fino a quando uno di questi non viene completato, anche quando non si attende il TimeoutAfter? – Kir

11

Qui è un esempio completamente lavorato basa sulla sommità 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 vantaggio principale della realizzazione in questa risposta è che sono stati aggiunti generici, per cui la funzione (o attività) può restituire un valore. Ciò significa che ogni funzione esistente può essere avvolto in una funzione di timeout, es .:

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, il suo genere non una buona pratica per avere le eccezioni sollevate nel codice durante il normale funzionamento, a meno che non dovete assolutamente:

  • Ogni volta che viene lanciata 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 anello chiuso.

Utilizzare questo codice solo se non è assolutamente possibile modificare la funzione che si sta chiamando in modo che scada dopo uno specifico TimeSpan.

Questa risposta è realmente applicabile solo quando si tratta di librerie di librerie di terze parti che non è possibile refactoring 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 si rispettare tale regola, il 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 c'è stato un timeout ragionevole dopo un po 'di tempo, la tua app si bloccherebbe per un periodo di tempo molto lungo (ad esempio 30 secondi), quindi visualizzerebbe un errore e continuerà sulla sua strada allegra, o riproverà.

-1

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

+0

Qualche codice per illustrare il tuo punto? – rayryeng

+0

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

5

Un altro modo per 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 al di 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 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; 
0

Una versione generica della risposta di @ Kevan sopra con Reactive Extensions.

con il programmatore opzionale:

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

BTW: Quando un timeout accade, un'eccezione di timeout verrà gettato

3

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

TimeSpan timeout = TimeSpan.FromSeconds(10); 

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

TaskCanceledException verrà lanciato in caso di un timeout.