Pregunta La forma correcta de implementar una tarea interminable. (Temporizadores vs tarea)


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?


75
2017-12-04 03:12


origen


Respuestas:


Yo usaría TPL Dataflow para esto (ya que estás usando .NET 4.5 y usa Task internamente). Puedes crear fácilmente un ActionBlock<TInput> que publica elementos a sí mismo después de que se procesa su acción y espera 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;
}

Elegí el ActionBlock<TInput> para tomar un 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 el ActionBlock<TInput> solo procesa por defecto uno artículo a la vez, por lo que está garantizado que solo se procesará una acción (es decir, no tendrá que lidiar con reentrada cuando llama al Post método de extensión de vuelta en sí mismo).

También he pasado el CancellationToken estructura tanto para el constructor de la ActionBlock<TInput> y al Task.Delay método llamada; 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 para almacenar el ITargetBlock<DateTimeoffset> interfaz Implementado por ActionBlock<TInput> (esta es la abstracción de mayor nivel que representa bloques que son consumidores, y desea poder desencadenar el consumo mediante 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ías usar TPL Dataflow aquí? Algunas razones:

Separación de intereses

los CreateNeverEndingTask método 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 TPL es el mismo para un Task, que es el grupo de hilos. Al usar el ActionBlock<TInput> para procesar su acción, así como una llamada a Task.Delay, estás cediendo el control del hilo que estabas usando cuando en realidad no estás haciendo nada. De acuerdo, esto en realidad lleva a algunos gastos generales cuando se genera el nuevo Taskeso 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 el DoWork la función en realidad puede hacerse agilizable (es decir, en que devuelve un Task), entonces puede (posiblemente) optimizar esto aún más ajustando el método de fábrica de arriba para tomar una Func<DateTimeOffset, CancellationToken, Task> en lugar de un Action<DateTimeOffset>, al igual que:

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 a través de su método (si acepta uno), que se hace aquí.

Eso significa que entonces tendrías 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 las preocupaciones aquí) StartWork método para dar cuenta de la nueva firma pasó a la 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);
}

88
2017-12-04 21:54



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 dentro del Task.Delay, en lugar de tener que esperar por el Thread.Sleep para terminar.

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

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


63
2017-12-04 03:33



Aquí es lo que se me ocurrió:

  • Heredar de NeverEndingTask y anular el ExecutionCore método con el trabajo que desea hacer.
  • Cambiando ExecutionLoopDelayMs le permite ajustar el tiempo entre bucles, p. si quieres usar un algoritmo de reducción.
  • Start/Stop proporcionar 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 del ActionBlock solución basada arriba.
  • 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();
    }
}

3
2018-06-07 00:29