Bonne façon de mettre en œuvre une tâche sans fin. (Minuteries vs tâche)


92

Ainsi, mon application doit effectuer une action presque en continu (avec une pause d'environ 10 secondes entre chaque exécution) tant que l'application est en cours d'exécution ou qu'une annulation est demandée. Le travail qu'il doit 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'effectue pas l'action avant la fin du "tick" précédent.

Ou devrais-je utiliser une tâche générale en mode LongRunning avec un jeton d'annulation et avoir une boucle while infinie régulière à l'intérieur, appelant l'action effectuant le travail avec un Thread de 10 secondes. En ce qui concerne le modèle async / await, je ne suis pas sûr qu'il 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 simplement utiliser une simple minuterie tout en utilisant sa propriété AutoReset, et appeler .Stop () pour l'annuler?


La tâche semble exagérée compte tenu de ce que vous essayez d'accomplir. en.wikipedia.org/wiki/KISS_principle . Arrêtez le minuteur au début d'OnTick (), vérifiez un booléen pour voir si vous devriez faire quelque chose sur non, travaillez, redémarrez le minuteur lorsque vous avez terminé.
Mike Trusov du

Réponses:


94

J'utiliserais TPL Dataflow pour cela (puisque vous utilisez .NET 4.5 et qu'il utilise en Taskinterne). 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é.

Tout d'abord, créez une usine qui créera votre tâche sans 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;
}

J'ai choisi le ActionBlock<TInput>pour prendre une DateTimeOffsetstructure ; vous devez passer un paramètre de type, et il pourrait aussi bien passer un état utile (vous pouvez changer la nature de l'état, si vous le souhaitez).

Notez également que, ActionBlock<TInput>par défaut, ne traite qu'un seul élément à la fois, vous êtes donc assuré qu'une seule action sera traitée (ce qui signifie que vous n'aurez pas à gérer la réentrance quand il rappelle la Postméthode d'extension sur elle-même).

J'ai également passé la CancellationTokenstructure au constructeur du ActionBlock<TInput>et à l' appel de Task.Delayméthode ; si le processus est annulé, l'annulation aura lieu à la première occasion possible.

À partir de là, il s'agit d'un refactoring facile de votre code pour stocker l' ITargetBlock<DateTimeoffset>interface implémentée par ActionBlock<TInput>(c'est l'abstraction de niveau supérieur représentant les blocs qui sont des consommateurs, et vous voulez pouvoir déclencher la consommation via un appel à la Postméthode d'extension):

CancellationTokenSource wtoken;
ActionBlock<DateTimeOffset> task;

Votre StartWorkméthode:

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 puis votre StopWorkméthode:

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 CreateNeverEndingTaskméthode est maintenant une usine qui crée votre "service" pour ainsi dire. Vous contrôlez quand il démarre et s'arrête, et il est complètement autonome. Vous n'avez pas à associer le contrôle d'état de la minuterie à d'autres aspects de votre code. Vous créez simplement le bloc, démarrez-le et arrêtez-le 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 a 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 thread que vous utilisiez lorsque vous ne faites rien. Certes, cela entraîne en fait une surcharge lorsque vous créez le nouveau Taskqui traitera la continuation, mais cela devrait être petit, étant donné que vous ne traitez pas cela dans une boucle serrée (vous attendez dix secondes entre les invocations).

Si la DoWorkfonction peut réellement être rendue attendable (c'est-à-dire qu'elle renvoie a Task), vous pouvez (éventuellement) optimiser cela encore plus en modifiant la méthode d'usine ci-dessus pour prendre a Func<DateTimeOffset, CancellationToken, Task>au lieu de an Action<DateTimeOffset>, comme ceci:

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 bon d'intégrer CancellationTokenvotre méthode (si elle en accepte une), ce qui est fait ici.

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

Task DoWorkAsync(CancellationToken cancellationToken);

Vous devrez changer (seulement légèrement, et vous ne saignez pas la séparation des préoccupations ici) la StartWorkméthode pour tenir compte de la nouvelle signature transmise à la CreateNeverEndingTaskméthode, comme ceci:

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

Bonjour, j'essaye cette implémentation mais je suis confronté à des problèmes. Si mon DoWork ne prend aucun argument, task = CreateNeverEndingTask (now => DoWork (), wtoken.Token); me donne une erreur de construction (incompatibilité de type). D'un autre côté, si mon DoWork prend un paramètre DateTimeOffset, cette même ligne me donne une erreur de construction différente, me disant qu'aucune surcharge pour DoWork ne prend 0 argument. Pouvez-vous m'aider à comprendre celui-ci?
Bovaz

1
En fait, j'ai résolu mon problème en ajoutant un cast à la ligne où j'attribue la tâche et en passant le paramètre à DoWork: task = (ActionBlock <DateTimeOffset>) CreateNeverEndingTask (now => DoWork (now), wtoken.Token);
Bovaz

Vous pourriez également avoir changé le type de "tâche ActionBlock <DateTimeOffset>;" à la tâche ITargetBlock <DateTimeOffset>;
XOR

1
Je crois que cela est susceptible d'allouer de la mémoire pour toujours, conduisant finalement à un débordement.
Nate Gardner

@NateGardner Dans quelle partie?
casperOne

75

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

Il y a quelques petits ajustements que vous pouvez apporter à 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 si à l'intérieur du Task.Delay, plutôt que d'avoir à attendre la Thread.Sleepfin.

De plus, utiliser Task.Delayover Thread.Sleepsignifie que vous n'attachez pas un fil à ne rien faire pendant la durée du sommeil.

Si vous le pouvez, vous pouvez également faire DoWork()accepter un jeton d'annulation, et l'annulation sera beaucoup plus réactive.


1
Quelle tâche vous obtiendrez si vous utilisez le lambda asynchrone comme paramètre de Task.Factory.StartNew - blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx Lorsque vous effectuez une tâche. ); une fois l'annulation demandée, vous attendez une tâche incorrecte.
Lukas Pirkl

Oui, cela devrait en fait être Task.Run now, qui a la surcharge correcte.
porges du

Selon http://blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx, il semble qu'il Task.Runutilise le pool de threads, donc votre exemple d'utilisation Task.Runau lieu de Task.Factory.StartNewwith TaskCreationOptions.LongRunningne fait pas exactement la même chose - si j'avais besoin de la tâche pour utiliser l' LongRunningoption, ne pourrais-je pas utiliser Task.Runcomme vous l'avez montré, ou est-ce que je manque quelque chose?
Jeff

@Lumirris: Le but de async / await est d'éviter de bloquer un thread pendant tout le temps qu'il s'exécute (ici, pendant l'appel Delay, la tâche n'utilise pas de thread). Donc, utiliser LongRunningest un peu incompatible avec l'objectif de ne pas lier les threads. Si vous voulez garantir l' exécution sur son propre thread, vous pouvez l'utiliser, mais ici vous allez démarrer un thread qui est en veille la plupart du temps. Quel est le cas d'utilisation?
porges

@Porges Point pris. Mon cas d'utilisation serait une tâche exécutant une boucle infinie, dans laquelle chaque itération ferait un morceau de travail, et se «relaxerait» pendant 2 secondes avant de faire un autre morceau de travail sur l'itération suivante. Il fonctionne pour toujours, mais prend régulièrement des pauses de 2 secondes. Mon commentaire, cependant, portait davantage sur la question de savoir si vous pouviez le spécifier en LongRunningutilisant la Task.Runsyntaxe. D'après la documentation, il semble que la Task.Runsyntaxe est plus propre, tant que vous êtes satisfait des paramètres par défaut qu'elle utilise. Il ne semble pas y avoir de surcharge qui prend un TaskCreationOptionsargument.
Jeff

4

Voici ce que j'ai trouvé:

  • Héritez de NeverEndingTaskla ExecutionCoreméthode et remplacez-la par le travail que vous souhaitez effectuer.
  • La modification ExecutionLoopDelayMsvous permet d'ajuster le temps entre les boucles, par exemple si vous souhaitez utiliser un algorithme d'interruption.
  • Start/Stop fournir une interface synchrone pour démarrer / arrêter la tâche.
  • LongRunningsignifie que vous obtiendrez un thread dédié par NeverEndingTask.
  • Cette classe n'alloue pas de mémoire dans une boucle contrairement à la ActionBlocksolution basée ci-dessus.
  • Le code ci-dessous est un sketch, pas nécessairement du 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();
    }
}
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.