The right way to implement an endless task. (Timers versus task)

So, my application needs to perform the action almost continuously (with a pause of 10 seconds or so between each run) until the application is started or cancellation is required. The work that he must do has the ability to take up to 30 seconds.

Is it better to use System.Timers.Timer and use AutoReset to make sure that it did not perform the action before the previous tick completed.

Or should I use a generic LongRunning task with a cancellation token and have a regular, infinite while loop inside it that triggers an action that does work with a 10 second Thread.Sleep between calls? As for the async / await model, I'm not sure if this would be appropriate here, as I have no return values ​​from work.

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

or just use a simple timer when using its AutoReset property and call .Stop () to cancel it?

+71
multithreading c # timer task-parallel-library
Dec 04 '12 at 3:12
source share
3 answers

I would use TPL Dataflow for this (since you are using .NET 4.5 and using Task internally). You can easily create an ActionBlock<TInput> that sends elements to itself after it has processed this action and waited for the appropriate amount of time.

First create a factory that will create your endless task:

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

I chose ActionBlock<TInput> to create the DateTimeOffset structure; you need to pass a type parameter, and it can also pass some useful state (you can change the nature of the state if you want).

Also note that the ActionBlock<TInput> only processes one element at a time, so you are guaranteed that only one action will be processed (which means you don't have to deal with reentrancy when it calls the Post method on its own) .

I also passed the CancellationToken structure to both the ActionBlock<TInput> constructor and the ActionBlock<TInput> constructor call method ; if the process is canceled, the cancellation will be performed as soon as possible.

From there, this is a simple refactoring of your code to store the ITargetBlock<DateTimeoffset> interface implemented by ActionBlock<TInput> (this is a higher level abstraction representing consumer blocks and you want to be able to trigger consumption by calling the Post extension method):

 CancellationTokenSource wtoken; ActionBlock<DateTimeOffset> task; 

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

And then your StopWork method:

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

Why do you want to use TPL Dataflow here? A few reasons:

Separation of problems

Now the CreateNeverEndingTask method is a factory that creates your "service", so to speak. You control when it starts and stops, and it is completely self-sufficient. You do not need to interweave timer status monitoring with other aspects of your code. You simply create a block, start it and stop when you are done.

More efficient use of threads / tasks / resources

The default scheduler for blocks in a TPL data stream is the same for Task , which is a thread pool. Using the ActionBlock<TInput> to handle your action, as well as calling Task.Delay , you gain control over the thread that you used when you were actually doing nothing. Of course, this leads to some overhead when you create a new Task that will handle the continuation, but this should be small, given that you do not process this in a narrow loop (you expect ten seconds between calls).

If the DoWork function DoWork really be made expected (namely, that it returns a Task ), then you can (possibly) optimize it even more by changing the factory method above to take Func<DateTimeOffset, CancellationToken, Task> instead of Action<DateTimeOffset> , eg:

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

Of course, it would be good practice to weave the CancellationToken using your method (if it accepts it), which is done here.

This means that you will have a DoWorkAsync method with the following signature:

 Task DoWorkAsync(CancellationToken cancellationToken); 

You would need to change (just a little, and you don't StartWork off problem sharing) the StartWork method to account for the new signature passed to the CreateNeverEndingTask method, for example:

 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); } 
+83
Dec 04
source share

I believe that the new task-based interface is very simple to perform such actions - even easier than using the Timer class.

You can make minor adjustments to your example. Instead:

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

You can do it:

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

Thus, cancellation will occur instantly if inside Task.Delay , instead of waiting for Task.Delay to complete.

Also, using Task.Delay over Task.Delay means that you are not tying a thread that does nothing during the whole sleep time.

If you are able, you can also make DoWork() accept the cancellation token, and the cancellation will be much more responsive.

+58
Dec 04
source share

Here is what I came up with:

  • Inherit from NeverEndingTask and override the ExecutionCore method with the work you want to do.
  • Changing ExecutionLoopDelayMs allows you to adjust the time between cycles, for example. if you want to use the deferral algorithm.
  • Start/Stop provides a synchronous interface for starting / stopping a task.
  • LongRunning means that you will get one dedicated thread for NeverEndingTask .
  • This class does not allocate memory in a loop, unlike the above ActionBlock solution.
  • The code below is a sketch, not necessarily a production code :)

:

 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
Jun 07 '16 at 0:29
source share



All Articles