web-dev-qa-db-fra.com

Manière correcte d'implémenter une tâche sans fin. (Timers vs Task)

Ainsi, mon application doit effectuer une action presque en continu (avec une pause de 10 secondes environ entre chaque exécution) aussi longtemps que l'application est en cours d'exécution ou qu'une annulation est demandée. Le travail à effectuer peut prendre jusqu'à 30 secondes.

Est-il préférable d’utiliser un System.Timers.Timer et d’utiliser AutoReset pour s’assurer qu’il n’exécute pas l’action avant la fin de la "coche" précédente.

Ou devrais-je utiliser une tâche générale en mode LongRunning avec un jeton d'annulation et avoir une boucle infinie régulière en appelant l'action pour effectuer le travail avec un Thread de 10 secondes. Entre deux appels? En ce qui concerne le modèle async/wait, je ne suis pas sûr que cela conviendrait ici car je n’ai aucune valeur de retour du travail.

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

ou utilisez-vous simplement une minuterie lorsque vous utilisez sa propriété AutoReset et appelez .Stop () pour l'annuler?

83
Josh

J'utiliserais TPL Dataflow pour cela (puisque vous utilisez .NET 4.5 et qu'il utilise Task en interne). Vous pouvez facilement créer un ActionBlock<TInput> qui publie des éléments sur lui-même après avoir traité son action et attendu un laps de temps approprié.

Commencez par créer une fabrique qui créera votre tâche permanente:

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

J'ai choisi le ActionBlock<TInput> prendre une structure DateTimeOffset ; vous devez transmettre un paramètre de type, qui peut également transmettre un état utile (vous pouvez modifier la nature de l'état, si vous le souhaitez).

Notez également que le ActionBlock<TInput> _ traite par défaut uniquement un élément à la fois, vous avez ainsi la garantie de ne traiter qu'une seule action (ce qui signifie que vous n'aurez pas à vous occuper de réentrance quand il appelle la méthode Post extension sur elle-même).

J'ai également passé la structure CancellationToken au constructeur du ActionBlock<TInput> et aux Task.Delay méthode appelez; si le processus est annulé, l'annulation aura lieu à la première occasion possible.

A partir de là, il est facile de refactoriser votre code pour stocker le ITargetBlock<DateTimeoffset> interface implémenté par ActionBlock<TInput> (il s’agit de l’abstraction de niveau supérieur représentant les blocs consommateurs, et vous voulez pouvoir déclencher la consommation via un appel à la méthode d’extension Post):

CancellationTokenSource wtoken;
ActionBlock<DateTimeOffset> task;

Votre méthode 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);
}

Et ensuite votre méthode 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;
}

Pourquoi voudriez-vous utiliser TPL Dataflow ici? Quelques raisons:

Séparation des préoccupations

La méthode CreateNeverEndingTask est maintenant une fabrique qui crée votre "service" pour ainsi dire. Vous contrôlez le début et la fin, et tout est autonome. Vous n'avez pas à mêler le contrôle d'état du minuteur à d'autres aspects de votre code. Vous créez simplement le bloc, le démarrez et l'arrêtez lorsque vous avez terminé.

Utilisation plus efficace des threads/tâches/ressources

Le planificateur par défaut pour les blocs dans le flux de données TPL est le même pour un Task, qui est le pool de threads. En utilisant le ActionBlock<TInput> pour traiter votre action, ainsi qu'un appel à Task.Delay, vous cédez le contrôle du fil que vous utilisiez alors que vous ne faites rien. Certes, cela entraîne en fait une surcharge lorsque vous créez le nouveau Task qui traitera la suite, mais cela devrait être minime, étant donné que vous ne traitez pas cela dans une boucle serrée (vous attendez dix secondes entre invocations).

Si la fonction DoWork peut être rendue attendue (c'est-à-dire qu'elle renvoie un Task), vous pouvez (éventuellement) l'optimiser encore davantage en modifiant légèrement la méthode d'usine ci-dessus pour - Func<DateTimeOffset, CancellationToken, Task> au lieu d'un Action<DateTimeOffset>, ainsi:

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

Bien sûr, il serait judicieux de lier le CancellationToken à votre méthode (si elle en accepte un), ce qui est fait ici.

Cela signifie que vous auriez alors une méthode DoWorkAsync avec la signature suivante:

Task DoWorkAsync(CancellationToken cancellationToken);

Il vous faudrait changer (légèrement, et vous ne saignez pas la séparation des problèmes ici) la méthode StartWork pour prendre en compte la nouvelle signature transmise à la méthode CreateNeverEndingTask, comme suit:

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

Je trouve la nouvelle interface basée sur les tâches très simple pour faire des choses comme celle-ci - encore plus facile que d’utiliser la classe Timer.

Vous pouvez apporter quelques modifications à votre exemple. Au lieu de:

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

Tu peux le faire:

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

De cette façon, l'annulation se produira instantanément à l'intérieur du Task.Delay, Plutôt que d'attendre la fin du Thread.Sleep.

De plus, l'utilisation de Task.Delay Sur Thread.Sleep Signifie que vous ne bloquez pas un fil qui ne fait rien pendant la durée du sommeil.

Si vous le pouvez, vous pouvez également faire en sorte que DoWork() accepte un jeton d'annulation. Cette annulation sera beaucoup plus sensible.

71
porges

Voici ce que je suis venu avec:

  • Héritez de NeverEndingTask et remplacez la méthode ExecutionCore par le travail que vous souhaitez effectuer.
  • Changer ExecutionLoopDelayMs vous permet de régler le temps entre les boucles, par exemple. si vous vouliez utiliser un algorithme de backoff.
  • Start/Stop fournit une interface synchrone pour démarrer/arrêter la tâche.
  • LongRunning signifie que vous obtiendrez un fil dédié par NeverEndingTask.
  • Cette classe n'alloue pas de mémoire dans une boucle contrairement à la solution ci-dessus basée sur ActionBlock.
  • Le code ci-dessous est un sketch, pas nécessairement un code de production :)

:

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