c# multithreading timer task-parallel-library .net-4.5

c# - La forma correcta de implementar una tarea interminable.(Temporizadores vs tarea)



multithreading timer (3)

Aquí es lo que se me ocurrió:

  • Heredar de NeverEndingTask y anular el método ExecutionCore con el trabajo que desea hacer.
  • Cambiar ExecutionLoopDelayMs permite ajustar el tiempo entre bucles, por ejemplo, si desea utilizar un algoritmo de reducción.
  • Start/Stop proporciona una interfaz síncrona para iniciar / detener la tarea.
  • LongRunning significa que obtendrá un hilo dedicado por NeverEndingTask .
  • Esta clase no asigna memoria en un bucle a diferencia de la solución basada en ActionBlock anterior.
  • El código a continuación es boceto, no necesariamente código de producción :)

:

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

Por lo tanto, mi aplicación necesita realizar una acción de forma casi continua (con una pausa de 10 segundos más o menos entre cada ejecución) mientras la aplicación se esté ejecutando o se solicite una cancelación. El trabajo que necesita hacer tiene la posibilidad de tomar hasta 30 segundos.

¿Es mejor utilizar un Temporizador System.Timers y usar AutoReset para asegurarse de que no realiza la acción antes de que se complete el "tick" anterior?

¿O debería usar una tarea general en el modo LongRunning con un token de cancelación, y tener un bucle while infinito dentro de él llamando a la acción que hace el trabajo con un Thread de 10 segundos. ¿Duerme entre llamadas? En cuanto al modelo async / await, no estoy seguro de que sea apropiado aquí ya que no tengo ningún valor de retorno del trabajo.

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 simplemente use un temporizador simple mientras usa su propiedad AutoReset, y llame a .Stop () para cancelarlo?


Encuentro que la nueva interfaz basada en tareas es muy simple para hacer cosas como esta, incluso más fácil que usar la clase Timer.

Hay algunos pequeños ajustes que puede hacer a su ejemplo. En lugar de:

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

Puedes hacerlo:

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

De esta forma, la cancelación ocurrirá instantáneamente si está dentro de Task.Delay , en lugar de tener que esperar a que termine Thread.Sleep .

Además, al usar Task.Delay over Thread.Sleep significa que no está atando un hilo sin hacer nada mientras duerme.

Si puede, también puede hacer que DoWork() acepte un token de cancelación, y la cancelación será mucho más receptiva.


Usaría TPL Dataflow para esto (ya que estás usando .NET 4.5 y usa la Task internamente). Puede crear fácilmente un ActionBlock<TInput> que publique elementos a sí mismo después de que se procese su acción y haya esperado una cantidad de tiempo adecuada.

Primero, crea una fábrica que creará tu tarea interminable:

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

He elegido ActionBlock<TInput> para tomar una estructura DateTimeOffset ; 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> procesa de manera predeterminada solo un elemento a la vez, por lo que tiene la garantía de que solo se procesará una acción (es decir, no tendrá que lidiar con la reentrancy cuando llame al método de extensión de Post de vuelta en sí mismo).

También pasé la estructura CancellationToken tanto al constructor de ActionBlock<TInput> como a la Task.Delay método Task.Delay ; si el proceso se cancela, la cancelación se realizará en la primera oportunidad posible.

A partir de ahí, es una fácil refacturación de su código almacenar la ITargetBlock<DateTimeoffset> 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 un llamar al método de extensión de Post ):

CancellationTokenSource wtoken; ActionBlock<DateTimeOffset> task;

Su método StartWork :

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 su método 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; }

¿Por qué querrías usar TPL Dataflow aquí? Algunas razones:

Separación de intereses

El método CreateNeverEndingTask ahora es una fábrica que crea su "servicio", por así decirlo. Usted controla cuando comienza y se detiene, y es completamente autónomo. No tiene que entrelazar el control 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 programador predeterminado para los bloques en el flujo de datos de TPL es el mismo para una Task , que es el grupo de subprocesos. Al usar ActionBlock<TInput> para procesar su acción, así como una llamada a Task.Delay , está cediendo el control del hilo que estaba usando cuando no está haciendo nada realmente. Por supuesto, esto realmente genera cierta sobrecarga cuando genera la nueva Task que procesará la continuación, pero debe ser pequeña, considerando que no está procesando esto en un ciclo cerrado (está esperando diez segundos entre invocaciones).

Si la función DoWork puede hacerse realmente a la espera (es decir, devuelve una Task ), entonces (posiblemente) optimice esto aún más ajustando el método de fábrica anterior para tomar un Func<DateTimeOffset, CancellationToken, Task> lugar de un 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 tejer el CancellationToken través de su método (si acepta uno), que se hace aquí.

Eso significa que entonces DoWorkAsync un método DoWorkAsync con la siguiente firma:

Task DoWorkAsync(CancellationToken cancellationToken);

Tendría que cambiar (solo un poco, y no está desangrando la separación de las preocupaciones aquí) el método StartWork para dar cuenta de la nueva firma pasada al método CreateNeverEndingTask , 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); }