Userei TPL Dataflow per questo (dal momento che si sta utilizzando .NET 4.5 e utilizza Task
internamente). 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 DateTimeOffset
struttura ; 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 Post
metodo di estensione su se stesso).
Ho anche passato la CancellationToken
struttura sia al costruttore del ActionBlock<TInput>
che alla chiamata al Task.Delay
metodo ; 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 Post
metodo di estensione):
CancellationTokenSource wtoken;
ActionBlock<DateTimeOffset> task;
Il 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 StopWork
metodo:
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 CreateNeverEndingTask
metodo 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 Task
che 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 DoWork
funzione 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 CancellationToken
metodo con il tuo metodo (se lo accetta), che viene fatto qui.
Ciò significa che avresti quindi un DoWorkAsync
metodo con la seguente firma:
Task DoWorkAsync(CancellationToken cancellationToken);
Dovresti cambiare (solo leggermente, e qui non stai eliminando la separazione delle preoccupazioni) il StartWork
metodo per tenere conto della nuova firma passata al CreateNeverEndingTask
metodo, 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);
}