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?
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);
}
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.
Voici ce que je suis venu avec:
NeverEndingTask
et remplacez la méthode ExecutionCore
par le travail que vous souhaitez effectuer.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
.ActionBlock
.:
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();
}
}