Manera adecuada de implementar una tarea interminable. (Temporizadores vs Tarea)

92

Por lo tanto, mi aplicación debe realizar una acción casi continuamente (con una pausa de 10 segundos aproximadamente 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 llevar hasta 30 segundos.

¿Es mejor usar un System.Timers.Timer y usar AutoReset para asegurarse de que no realice 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 regular infinito dentro de él llamando a la acción que hace el trabajo con un Thread.Sleep de 10 segundos 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 usa un temporizador simple mientras usa su propiedad AutoReset y llama a .Stop () para cancelarlo?

Josh
fuente
La tarea parece una exageración considerando lo que está tratando de lograr. en.wikipedia.org/wiki/KISS_principle . Detenga el temporizador al inicio de OnTick (), marque un bool para ver si debería estar haciendo algo en no, haga el trabajo, reinicie el temporizador cuando haya terminado.
Mike Trusov

Respuestas:

94

Que haría uso de TPL de flujo de datos para esto (ya que usted está utilizando .NET 4.5 y utiliza Taskinternamente). 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 DateTimeOffsetestructura ; 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 Postmétodo de extensión ).

También pasé la CancellationTokenestructura tanto al constructor del ActionBlock<TInput>como a la llamada al Task.Delaymé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 Postmétodo de extensión):

CancellationTokenSource wtoken;
ActionBlock<DateTimeOffset> task;

Tu StartWorkmé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 StopWorkmé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 CreateNeverEndingTaskmé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 Taskque 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 DoWorkfunció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 CancellationTokenpasar por alto su método (si acepta uno), que se hace aquí.

Eso significa que entonces tendría un DoWorkAsyncmé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 StartWorkmétodo para dar cuenta de la nueva firma pasada al CreateNeverEndingTaskmé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);
}
casperOne
fuente
Hola, estoy probando esta implementación pero tengo problemas. Si mi DoWork no acepta argumentos, task = CreateNeverEndingTask (ahora => DoWork (), wtoken.Token); me da un error de compilación (falta de coincidencia de tipos). Por otro lado, si mi DoWork toma un parámetro DateTimeOffset, esa misma línea me da un error de compilación diferente, diciéndome que ninguna sobrecarga para DoWork toma 0 argumentos. ¿Podrías ayudarme a resolver esto?
Bovaz
1
En realidad, resolví mi problema agregando un molde a la línea donde asigno la tarea y pasando el parámetro a DoWork: task = (ActionBlock <DateTimeOffset>) CreateNeverEndingTask (ahora => DoWork (ahora), wtoken.Token);
Bovaz
También puede cambiar el tipo de "tarea ActionBlock <DateTimeOffset>"; a la tarea ITargetBlock <DateTimeOffset>;
XOR
1
Creo que es probable que esto asigne memoria para siempre, lo que eventualmente conducirá a un desbordamiento.
Nate Gardner
@NateGardner ¿En qué parte?
casperOne
75

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 en su ejemplo. En vez 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 manera la cancelación ocurrirá instantáneamente si está dentro del Task.Delay, en lugar de tener que esperar a Thread.Sleepque termine.

Además, usar Task.Delayover Thread.Sleepsignifica que no está atando un hilo sin hacer nada durante la duración del sueño.

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

porges
fuente
1
Vea qué tarea obtendrá si usa la lambda asíncrona como parámetro de Task.Factory.StartNew - blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx Cuando haga la tarea. Espere ( ); después de que se solicite la cancelación, estará esperando la tarea incorrecta.
Lukas Pirkl
Sí, debería ser Task.Run ahora, que tiene la sobrecarga correcta.
Porges
De acuerdo con http://blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx , parece que Task.Runusa el grupo de subprocesos, por lo que su ejemplo usando en Task.Runlugar de Task.Factory.StartNewcon TaskCreationOptions.LongRunningno hace exactamente lo mismo - si necesitara la tarea para usar la LongRunningopción, ¿no podría usarla Task.Runcomo ha mostrado o me falta algo?
Jeff
@Lumirris: El objetivo de async / await es evitar atar un hilo durante todo el tiempo que se está ejecutando (aquí, durante la llamada Delay, la tarea no está usando un hilo). Por lo tanto, usar LongRunninges algo incompatible con el objetivo de no atar hilos. Si desea garantizar que se ejecute en su propio hilo, puede usarlo, pero aquí va a iniciar un hilo que está inactivo la mayor parte del tiempo. ¿Cuál es el caso de uso?
Porges
@Porges Point tomado. Mi caso de uso sería una tarea que ejecuta un bucle infinito, en el que cada iteración haría una gran cantidad de trabajo, y se 'relajaría' durante 2 segundos antes de hacer otro trabajo en la siguiente iteración. Funciona para siempre, pero toma descansos regulares de 2 segundos. Mi comentario, sin embargo, fue más sobre si podría especificar que se LongRunninguse la Task.Runsintaxis. Según la documentación, parece que Task.Runtiene una sintaxis más limpia, siempre que esté satisfecho con la configuración predeterminada que utiliza. No parece haber una sobrecarga que requiera una TaskCreationOptionsdiscusión.
Jeff
4

Esto es lo que se me ocurrió:

  • Herede NeverEndingTasky anule el ExecutionCoremétodo con el trabajo que desea hacer.
  • Cambiar le ExecutionLoopDelayMspermite ajustar el tiempo entre bucles, por ejemplo, si desea utilizar un algoritmo de retroceso.
  • Start/Stop proporcionar una interfaz síncrona para iniciar / detener la tarea.
  • LongRunningsignifica que obtendrá un hilo dedicado por NeverEndingTask.
  • Esta clase no asigna memoria en un ciclo a diferencia de la ActionBlocksolución basada arriba.
  • El siguiente código es un boceto, no necesariamente un 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();
    }
}
Jack Ukleja
fuente