Que haría uso de TPL de flujo de datos para esto (ya que usted está utilizando .NET 4.5 y utiliza Task
internamente). Puede crear fácilmente un elemento ActionBlock<TInput>
que se publicará a sí mismo después de que se procesó su acción y se esperó una cantidad de tiempo adecuada.
Primero, crea una fábrica que creará tu tarea sin fin:
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;
}
Elegí el ActionBlock<TInput>
para tomar una DateTimeOffset
estructura ; tiene que pasar un parámetro de tipo, y también podría pasar algún estado útil (puede cambiar la naturaleza del estado, si lo desea).
Además, tenga en cuenta que, ActionBlock<TInput>
de forma predeterminada, procesa solo un elemento a la vez, por lo que tiene la garantía de que solo se procesará una acción (lo que significa que no tendrá que lidiar con la reentrada cuando vuelva a llamar al Post
método de extensión ).
También pasé la CancellationToken
estructura tanto al constructor del ActionBlock<TInput>
como a la llamada al Task.Delay
método ; si se cancela el proceso, la cancelación se realizará en la primera oportunidad posible.
A partir de ahí, es una fácil refactorización de su código para almacenar la ITargetBlock<DateTimeoffset>
interfaz implementada por ActionBlock<TInput>
(esta es la abstracción de nivel superior que representa bloques que son consumidores, y desea poder desencadenar el consumo a través de una llamada al Post
método de extensión):
CancellationTokenSource wtoken;
ActionBlock<DateTimeOffset> task;
Tu StartWork
método:
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);
}
Y luego tu StopWork
método:
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;
}
¿Por qué querría utilizar TPL Dataflow aquí? Algunas razones:
Separación de intereses
El CreateNeverEndingTask
método es ahora una fábrica que crea su "servicio" por así decirlo. Tú controlas cuándo se inicia y se detiene, y es completamente autónomo. No tiene que entrelazar el control de estado del temporizador con otros aspectos de su código. Simplemente crea el bloque, inícielo y deténgalo cuando haya terminado.
Uso más eficiente de hilos / tareas / recursos
El planificador predeterminado para los bloques en el flujo de datos de TPL es el mismo para a Task
, que es el grupo de subprocesos. Al usar el ActionBlock<TInput>
para procesar su acción, así como una llamada a Task.Delay
, cede el control del hilo que estaba usando cuando en realidad no está haciendo nada. Por supuesto, esto en realidad genera algunos gastos generales cuando genera el nuevo Task
que procesará la continuación, pero eso debería ser pequeño, considerando que no está procesando esto en un ciclo cerrado (está esperando diez segundos entre invocaciones).
Si la DoWork
función realmente se puede convertir en esperable (es decir, porque devuelve a Task
), entonces puede (posiblemente) optimizar esto aún más ajustando el método de fábrica anterior para tomar a en Func<DateTimeOffset, CancellationToken, Task>
lugar de an Action<DateTimeOffset>
, así:
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;
}
Por supuesto, sería una buena práctica CancellationToken
pasar por alto su método (si acepta uno), que se hace aquí.
Eso significa que entonces tendría un DoWorkAsync
método con la siguiente firma:
Task DoWorkAsync(CancellationToken cancellationToken);
Tendría que cambiar (solo un poco, y no está desangrando la separación de preocupaciones aquí) el StartWork
método para dar cuenta de la nueva firma pasada al CreateNeverEndingTask
método, así:
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);
}