2012-12-04 11 views
73

Quindi, la mia app deve eseguire un'azione quasi ininterrottamente (con una pausa di 10 secondi circa tra una corsa e l'altra) per tutto il tempo in cui l'app è in esecuzione o viene richiesta una cancellazione. Il lavoro che deve fare ha la possibilità di richiedere fino a 30 secondi.Modo corretto per implementare un'attività senza fine. (Timers vs Task)

È meglio utilizzare un System.Timers.Timer e utilizzare AutoReset per assicurarsi che non esegua l'azione prima che il precedente "segno di spunta" sia stato completato.

Oppure dovrei utilizzare un'attività generale in modalità LongRunning con un token di annullamento e un ciclo while infinito regolare al suo interno chiamando l'azione che esegue il lavoro con un Thread.Sleep di 10 secondi tra una chiamata e l'altra? Per quanto riguarda il modello async/await, non sono sicuro che sarebbe appropriato in questo caso in quanto non ho alcun valore di ritorno dal lavoro.

CancellationTokenSource wtoken; 
Task task; 

void StopWork() 
{ 
    wtoken.Cancel(); 

    try 
    { 
     task.Wait(); 
    } catch(AggregateException) { } 
} 

void StartWork() 
{ 
    wtoken = new CancellationTokenSource(); 

    task = Task.Factory.StartNew(() => 
    { 
     while (true) 
     { 
      wtoken.Token.ThrowIfCancellationRequested(); 
      DoWork(); 
      Thread.Sleep(10000); 
     } 
    }, wtoken, TaskCreationOptions.LongRunning); 
} 

void DoWork() 
{ 
    // Some work that takes up to 30 seconds but isn't returning anything. 
} 

o semplicemente utilizzare un semplice timer mentre si utilizza la proprietà AutoReset e chiamare .Stop() per annullarlo?

+0

Task sembra un eccessivo considerando quello che si sta cercando di raggiungere. http://en.wikipedia.org/wiki/KISS_principle. Arresta il timer all'inizio di OnTick(), controlla un bool per vedere se dovresti fare qualcosa su no, fare il lavoro, riavviare Timer quando hai finito. –

risposta

87

Vorrei usare TPL Dataflow per questo (dato che stai usando .NET 4.5 e usa Task internamente). È possibile creare facilmente un ActionBlock<TInput> che pubblica gli articoli su se stesso dopo che è stato elaborato e ha atteso un intervallo di tempo appropriato.

In primo luogo, creare una fabbrica che creerà il vostro compito non finisce mai:

ITargetBlock<DateTimeOffset> CreateNeverEndingTask(
    Action<DateTimeOffset> action, CancellationToken cancellationToken) 
{ 
    // Validate parameters. 
    if (action == null) throw new ArgumentNullException("action"); 

    // Declare the block variable, it needs to be captured. 
    ActionBlock<DateTimeOffset> block = null; 

    // Create the block, it will call itself, so 
    // you need to separate the declaration and 
    // the assignment. 
    // Async so you can wait easily when the 
    // delay comes. 
    block = new ActionBlock<DateTimeOffset>(async now => { 
     // Perform the action. 
     action(now); 

     // Wait. 
     await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken). 
      // Doing this here because synchronization context more than 
      // likely *doesn't* need to be captured for the continuation 
      // here. As a matter of fact, that would be downright 
      // dangerous. 
      ConfigureAwait(false); 

     // Post the action back to the block. 
     block.Post(DateTimeOffset.Now); 
    }, new ExecutionDataflowBlockOptions { 
     CancellationToken = cancellationToken 
    }); 

    // Return the block. 
    return block; 
} 

ho scelto il ActionBlock<TInput> di prendere una DateTimeOffset structure; devi passare un parametro di tipo, e potrebbe anche passare qualche stato utile (puoi cambiare la natura dello stato, se vuoi).

Si noti inoltre che il ActionBlock<TInput> dai processi predefiniti solo uno elemento alla volta, in modo avrete la garanzia che solo un'azione sarà elaborato (significato, non avrete a che fare con reentrancy quando si chiama il Post extension method su se stesso).

Ho anche passato il CancellationToken structure a entrambi il costruttore dello ActionBlock<TInput> e alla chiamata Task.Delay method; se la procedura è annullata, la cancellazione avverrà alla prima opportunità possibile.

Da lì, è un semplice refactoring del codice per archiviare lo ITargetBlock<DateTimeoffset> interface implementato da ActionBlock<TInput> (questa è l'astrazione di livello superiore che rappresenta i blocchi che sono consumatori e si desidera essere in grado di attivare il consumo tramite una chiamata al Post metodo di estensione):

CancellationTokenSource wtoken; 
ActionBlock<DateTimeOffset> task; 

tuo StartWork metodo:

void StartWork() 
{ 
    // Create the token source. 
    wtoken = new CancellationTokenSource(); 

    // Set the task. 
    task = CreateNeverEndingTask(now => DoWork(), wtoken.Token); 

    // Start the task. Post the time. 
    task.Post(DateTimeOffset.Now); 
} 

E poi il tuo metodo di StopWork:

void StopWork() 
{ 
    // CancellationTokenSource implements IDisposable. 
    using (wtoken) 
    { 
     // Cancel. This will cancel the task. 
     wtoken.Cancel(); 
    } 

    // Set everything to null, since the references 
    // are on the class level and keeping them around 
    // is holding onto invalid state. 
    wtoken = null; 
    task = null; 
} 

Perché vorresti utilizzare TPL Dataflow qui? Un paio di motivi:

separazione degli interessi

Il metodo CreateNeverEndingTask è ora una fabbrica che crea il tuo "servizio" per così dire. Tu controlli quando inizia e si ferma, ed è completamente autonomo. Non è necessario intrecciare il controllo dello stato del timer con altri aspetti del codice. Basta creare il blocco, avviarlo e fermarlo quando hai finito.

uso più efficiente di fili/attività/risorse

Lo scheduler di default per i blocchi nel flusso di dati TPL è lo stesso per un Task, che è il pool di thread. Usando lo ActionBlock<TInput> per elaborare la tua azione, così come una chiamata allo Task.Delay, stai ottenendo il controllo del thread che stavi utilizzando quando non stai facendo nulla. Certo, questo porta in effetti a un certo overhead quando si genera il nuovo Task che elaborerà la continuazione, ma che dovrebbe essere piccolo, considerando che non lo si sta elaborando in un ciclo stretto (si stanno aspettando dieci secondi tra le invocazioni).

Se la funzione DoWork in realtà può essere fatta awaitable (vale a dire, in quanto restituisce una Task), allora si può (forse) ottimizzare questo ancora più modificando il metodo factory precedente per dare Func<DateTimeOffset, CancellationToken, Task> invece di un Action<DateTimeOffset>, come così:

ITargetBlock<DateTimeOffset> CreateNeverEndingTask(
    Func<DateTimeOffset, CancellationToken, Task> action, 
    CancellationToken cancellationToken) 
{ 
    // Validate parameters. 
    if (action == null) throw new ArgumentNullException("action"); 

    // Declare the block variable, it needs to be captured. 
    ActionBlock<DateTimeOffset> block = null; 

    // Create the block, it will call itself, so 
    // you need to separate the declaration and 
    // the assignment. 
    // Async so you can wait easily when the 
    // delay comes. 
    block = new ActionBlock<DateTimeOffset>(async now => { 
     // Perform the action. Wait on the result. 
     await action(now, cancellationToken). 
      // Doing this here because synchronization context more than 
      // likely *doesn't* need to be captured for the continuation 
      // here. As a matter of fact, that would be downright 
      // dangerous. 
      ConfigureAwait(false); 

     // Wait. 
     await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken). 
      // Same as above. 
      ConfigureAwait(false); 

     // Post the action back to the block. 
     block.Post(DateTimeOffset.Now); 
    }, new ExecutionDataflowBlockOptions { 
     CancellationToken = cancellationToken 
    }); 

    // Return the block. 
    return block; 
} 

Naturalmente, sarebbe buona norma tessere la CancellationToken attraverso il metodo (se accetta uno), che viene fatto qui.

Ciò significa che si dovrebbe quindi avere un metodo DoWorkAsync con la seguente firma:

Task DoWorkAsync(CancellationToken cancellationToken); 

dovreste cambiare (solo leggermente, e non sei sanguinamento dalla separazione degli interessi qui) il metodo StartWork per tenere conto della nuova firma passato al metodo CreateNeverEndingTask, in questo modo:

void StartWork() 
{ 
    // Create the token source. 
    wtoken = new CancellationTokenSource(); 

    // Set the task. 
    task = CreateNeverEndingTask((now, ct) => DoWorkAsync(ct), wtoken.Token); 

    // Start the task. Post the time. 
    task.Post(DateTimeOffset.Now, wtoken.Token); 
} 
+0

Ciao, sto provando questa implementazione ma sto affrontando problemi. Se il mio DoWork non accetta argomenti, task = CreateNeverEndingTask (now => DoWork(), wtoken.Token); mi dà un errore di compilazione (tipo mancata corrispondenza). D'altra parte, se il mio DoWork accetta un parametro DateTimeOffset, quella stessa riga mi dà un errore di compilazione diverso, che mi dice che nessun sovraccarico per DoWork richiede 0 argomenti. Per favore, aiutami a capire questo? – Bovaz

+1

In realtà, ho risolto il problema aggiungendo un cast alla riga in cui assegno l'attività e passando il parametro a DoWork: task = (ActionBlock ) CreateNeverEndingTask (now => DoWork (now), wtoken.Token); – Bovaz

+0

Potresti anche cambiare il tipo di "ActionBlock task"; task ITargetBlock ; – XOR

61

Trovo che la nuova interfaccia basata su attività sia molto semplice per eseguire operazioni come questa, persino più semplice dell'utilizzo della classe Timer.

Ci sono alcune piccole regolazioni che puoi apportare al tuo esempio. Invece di:

task = Task.Factory.StartNew(() => 
{ 
    while (true) 
    { 
     wtoken.Token.ThrowIfCancellationRequested(); 
     DoWork(); 
     Thread.Sleep(10000); 
    } 
}, wtoken, TaskCreationOptions.LongRunning); 

Si può fare questo:

task = Task.Run(async() => // <- marked async 
{ 
    while (true) 
    { 
     DoWork(); 
     await Task.Delay(10000, wtoken.Token); // <- await with cancellation 
    } 
}, wtoken.Token); 

In questo modo la cancellazione avverrà istantaneamente se all'interno del Task.Delay, piuttosto che dover attendere il Thread.Sleep per finire.

Inoltre, l'utilizzo di Task.Delay su Thread.Sleep significa che non si sta bloccando un thread senza fare nulla per la durata del sonno.

Se possibile, è anche possibile impostare DoWork() per accettare un token di cancellazione e la cancellazione sarà molto più rapida.

+0

Scopri quale compito otterrai se utilizzi il parametro lambda async come parametro di Task.Factory.StartNew - http://blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx Quando do task.Wait(); dopo l'annullamento è richiesto, si sarà in attesa di attività errate. –

+0

Sì, in realtà dovrebbe essere Task.Run, che ha il sovraccarico corretto. – porges

+0

In base a [http://blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx](http://blogs.msdn.com/b/pfxteam/archive/2011/10 /24/10229468.aspx) sembra che 'Task.Run' usi il pool di thread, quindi il tuo esempio usando' Task.Run' invece di 'Task.Factory.StartNew' con' TaskCreationOptions.LongRunning' non fa esattamente il stessa cosa - se avessi bisogno di usare l'opzione 'LongRunning', non sarei in grado di usare' Task.Run' come hai mostrato, o mi manca qualcosa? – Jeff

3

Ecco quello che mi si avvicinò con:

  • Eredita da NeverEndingTask e sovrascrive il metodo ExecutionCore con il lavoro che si desidera eseguire.
  • La modifica ExecutionLoopDelayMs consente di regolare l'intervallo di tempo tra i cicli, ad es. se si desidera utilizzare un algoritmo di backoff.
  • Start/Stop fornire un'interfaccia sincrona per avviare/interrompere l'attività.
  • LongRunning significa che otterrete un thread dedicato per NeverEndingTask.
  • Questa classe non assegna memoria in un ciclo a differenza della soluzione basata su ActionBlock sopra.
  • Il codice qui sotto è schizzo, non necessariamente il codice di produzione :)

:

public abstract class NeverEndingTask 
{ 
    // Using a CTS allows NeverEndingTask to "cancel itself" 
    private readonly CancellationTokenSource _cts = new CancellationTokenSource(); 

    protected NeverEndingTask() 
    { 
     TheNeverEndingTask = new Task(
      () => 
      { 
       // Wait to see if we get cancelled... 
       while (!_cts.Token.WaitHandle.WaitOne(ExecutionLoopDelayMs)) 
       { 
        // Otherwise execute our code... 
        ExecutionCore(_cts.Token); 
       } 
       // If we were cancelled, use the idiomatic way to terminate task 
       _cts.Token.ThrowIfCancellationRequested(); 
      }, 
      _cts.Token, 
      TaskCreationOptions.DenyChildAttach | TaskCreationOptions.LongRunning); 

     // Do not forget to observe faulted tasks - for NeverEndingTask faults are probably never desirable 
     TheNeverEndingTask.ContinueWith(x => 
     { 
      Trace.TraceError(x.Exception.InnerException.Message); 
      // Log/Fire Events etc. 
     }, TaskContinuationOptions.OnlyOnFaulted); 

    } 

    protected readonly int ExecutionLoopDelayMs = 0; 
    protected Task TheNeverEndingTask; 

    public void Start() 
    { 
     // Should throw if you try to start twice... 
     TheNeverEndingTask.Start(); 
    } 

    protected abstract void ExecutionCore(CancellationToken cancellationToken); 

    public void Stop() 
    { 
     // This code should be reentrant... 
     _cts.Cancel(); 
     TheNeverEndingTask.Wait(); 
    } 
} 
Problemi correlati