So, my app needs to perform an action almost continuously (with a pause of 10 seconds or so between each run) for as long as the app is running or a cancellation is requested. The work it needs to do has the possibility of taking up to 30 seconds.
Is it better to use a System.Timers.Timer and use AutoReset to make sure it doesn’t perform the action before the previous “tick” has completed.
Or should I use a general Task in LongRunning mode with a cancellation token, and have a regular infinite while loop inside it calling the action doing the work with a 10 second Thread.Sleep between calls? As for the async/await model, I’m not sure it would be appropriate here as I don’t have any return values from the 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 while using its AutoReset property, and call .Stop() to cancel it?
I’d use TPL Dataflow for this (since you’re using .NET 4.5 and it uses
Taskinternally). You can easily create anActionBlock<TInput>which posts items to itself after it’s processed it’s action and waited an appropriate amount of time.First, create a factory that will create your never-ending task:
I’ve chosen the
ActionBlock<TInput>to take aDateTimeOffsetstructure; you have to pass a type parameter, and it might as well pass some useful state (you can change the nature of the state, if you want).Also, note that the
ActionBlock<TInput>by default processes only one item at a time, so you’re guaranteed that only one action will be processed (meaning, you won’t have to deal with reentrancy when it calls thePostextension method back on itself).I’ve also passed the
CancellationTokenstructure to both the constructor of theActionBlock<TInput>and to theTask.Delaymethod call; if the process is cancelled, the cancellation will take place at the first possible opportunity.From there, it’s an easy refactoring of your code to store the
ITargetBlock<DateTimeoffset>interface implemented byActionBlock<TInput>(this is the higher-level abstraction representing blocks that are consumers, and you want to be able to trigger the consumption through a call to thePostextension method):Your
StartWorkmethod:And then your
StopWorkmethod:Why would you want to use TPL Dataflow here? A few reasons:
Separation of concerns
The
CreateNeverEndingTaskmethod is now a factory that creates your “service” so to speak. You control when it starts and stops, and it’s completely self-contained. You don’t have to interweave state control of the timer with other aspects of your code. You simply create the block, start it, and stop it when you’re done.More efficient use of threads/tasks/resources
The default scheduler for the blocks in TPL data flow is the same for a
Task, which is the thread pool. By using theActionBlock<TInput>to process your action, as well as a call toTask.Delay, you’re yielding control of the thread that you were using when you’re not actually doing anything. Granted, this actually leads to some overhead when you spawn up the newTaskthat will process the continuation, but that should be small, considering you aren’t processing this in a tight loop (you’re waiting ten seconds between invocations).If the
DoWorkfunction actually can be made awaitable (namely, in that it returns aTask), then you can (possibly) optimize this even more by tweaking the factory method above to take aFunc<DateTimeOffset, CancellationToken, Task>instead of anAction<DateTimeOffset>, like so:Of course, it would be good practice to weave the
CancellationTokenthrough to your method (if it accepts one), which is done here.That means you would then have a
DoWorkAsyncmethod with the following signature:You’d have to change (only slightly, and you’re not bleeding out separation of concerns here) the
StartWorkmethod to account for the new signature passed to theCreateNeverEndingTaskmethod, like so: