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