Modo corretto per implementare un'attività senza fine. (Timer vs attività)


92

Pertanto, la mia app deve eseguire un'azione quasi continuamente (con una pausa di circa 10 secondi tra ogni esecuzione) 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.

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

O dovrei usare un'attività generale in modalità LongRunning con un token di annullamento e avere un ciclo while infinito regolare al suo interno che chiama l'azione che fa il lavoro con un thread di 10 secondi. Per quanto riguarda il modello async / await, non sono sicuro che sarebbe appropriato qui perché 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 usa un semplice timer mentre usi la sua proprietà AutoReset e chiama .Stop () per cancellarlo?


Il compito sembra eccessivo considerando ciò che stai cercando di ottenere. en.wikipedia.org/wiki/KISS_principle . Ferma il timer all'inizio di OnTick (), controlla un bool per vedere se dovresti fare qualcosa su not, funziona, riavvia il Timer quando hai finito.
Mike Trusov

Risposte:


94

Userei TPL Dataflow per questo (dal momento che si sta utilizzando .NET 4.5 e utilizza Taskinternamente). Puoi facilmente creare un ActionBlock<TInput>messaggio che inserisce gli elementi su se stesso dopo aver elaborato la sua azione e aver atteso un periodo di tempo appropriato.

Per prima cosa, crea una fabbrica che creerà il tuo compito senza fine:

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 ActionBlock<TInput>di prendere una DateTimeOffsetstruttura ; devi passare un parametro di tipo, e potrebbe anche passare uno stato utile (puoi cambiare la natura dello stato, se vuoi).

Inoltre, tieni presente che ActionBlock<TInput>per impostazione predefinita elabora solo un elemento alla volta, quindi hai la garanzia che verrà elaborata solo un'azione (il che significa che non dovrai affrontare il rientro quando richiama il Postmetodo di estensione su se stesso).

Ho anche passato la CancellationTokenstruttura sia al costruttore del ActionBlock<TInput>che alla chiamata al Task.Delaymetodo ; in caso di annullamento del processo, l'annullamento avverrà alla prima occasione possibile.

Da lì, è un facile refactoring del tuo codice per memorizzare l' ITargetBlock<DateTimeoffset>interfaccia implementata da ActionBlock<TInput>(questa è l'astrazione di livello superiore che rappresenta i blocchi che sono consumatori e vuoi essere in grado di attivare il consumo attraverso una chiamata al Postmetodo di estensione):

CancellationTokenSource wtoken;
ActionBlock<DateTimeOffset> task;

Il tuo StartWorkmetodo:

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 StopWorkmetodo:

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é dovresti utilizzare TPL Dataflow qui? Alcuni motivi:

Separazione degli interessi

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

Uso più efficiente di thread / attività / risorse

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

Se la DoWorkfunzione può effettivamente essere resa attendibile (vale a dire, in quanto restituisce a Task), allora puoi (possibilmente) ottimizzarla ancora di più modificando il metodo factory sopra per prendere a Func<DateTimeOffset, CancellationToken, Task>invece di un Action<DateTimeOffset>, in questo modo:

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 una buona pratica intrecciare il CancellationTokenmetodo con il tuo metodo (se lo accetta), che viene fatto qui.

Ciò significa che avresti quindi un DoWorkAsyncmetodo con la seguente firma:

Task DoWorkAsync(CancellationToken cancellationToken);

Dovresti cambiare (solo leggermente, e qui non stai eliminando la separazione delle preoccupazioni) il StartWorkmetodo per tenere conto della nuova firma passata al CreateNeverEndingTaskmetodo, 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);
}

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

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

Potresti anche cambiare il tipo di "ActionBlock <DateTimeOffset> task;" all'attività ITargetBlock <DateTimeOffset>;
XOR

1
Credo che questo possa allocare la memoria per sempre, portando così alla fine a un overflow.
Nate Gardner il

@NateGardner In quale parte?
casper L'

75

Trovo che la nuova interfaccia basata su attività sia molto semplice per fare cose come questa, persino più facile che usare la classe Timer.

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

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

Puoi farlo:

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 della Task.Delay, anziché dover attendere Thread.Sleepche finisca.

Inoltre, usare Task.Delayover Thread.Sleepsignifica che non stai legando un thread senza fare nulla per la durata del sonno.

Se puoi, puoi anche DoWork()accettare un token di cancellazione e la cancellazione sarà molto più reattiva.


1
Guarda quale attività otterrai se utilizzi lambda asincrono come parametro di Task.Factory.StartNew - blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx Quando esegui task.Wait ( ); dopo la richiesta di annullamento, sarai in attesa di un'attività errata.
Lukas Pirkl

Sì, questo dovrebbe effettivamente essere Task.Run now, che ha il corretto sovraccarico.
porges

Secondo http://blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx sembra che Task.Runusi il pool di thread, quindi il tuo esempio che usa Task.Runinvece di Task.Factory.StartNewcon TaskCreationOptions.LongRunningnon fa esattamente la stessa cosa - se avessi bisogno dell'attività per utilizzare l' LongRunningopzione, non sarei in grado di utilizzare Task.Runcome hai mostrato o mi manca qualcosa?
Jeff

@Lumirris: il punto di async / await è evitare di legare un thread per tutto il tempo in cui è in esecuzione (qui, durante la chiamata Delay l'attività non utilizza un thread). Quindi l'utilizzo LongRunningè un po 'incompatibile con l'obiettivo di non legare i thread. Se vuoi garantire l' esecuzione sul proprio thread, puoi usarlo, ma qui inizierai un thread che sta dormendo la maggior parte del tempo. Qual è il caso d'uso?
porges

@Porges Punto preso. Il mio caso d'uso sarebbe un'attività che esegue un ciclo infinito, in cui ogni iterazione farebbe un pezzo di lavoro e si "rilasserebbe" per 2 secondi prima di eseguire un altro chuck di lavoro nell'iterazione successiva. Funziona per sempre, ma richiede pause regolari di 2 secondi. Il mio commento, tuttavia, era più sul fatto che fosse possibile specificarlo LongRunningutilizzando la Task.Runsintassi. Dalla documentazione, sembra che la Task.Runsintassi sia più pulita, purché tu sia soddisfatto delle impostazioni predefinite che utilizza. Non sembra esserci un sovraccarico che richiede un TaskCreationOptionsargomento.
Jeff

4

Ecco cosa mi è venuto in mente:

  • Eredita da NeverEndingTaske sovrascrivi il ExecutionCoremetodo con il lavoro che desideri eseguire.
  • La modifica ExecutionLoopDelayMsconsente di regolare il tempo tra i loop, ad esempio se si desidera utilizzare un algoritmo di backoff.
  • Start/Stop fornire un'interfaccia sincrona per avviare / arrestare l'attività.
  • LongRunningsignifica che otterrai un thread dedicato per NeverEndingTask.
  • Questa classe non alloca la memoria in un ciclo a differenza della ActionBlocksoluzione basata sopra.
  • Il codice seguente è uno schizzo, non necessariamente un 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();
    }
}
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.