Until now I used a LongRunning TPL task for cyclic CPU bound background work instead of the threading timer, because:
- the TPL task supports cancellation
- the threading timer could start another thread while the programm is shutting down causing possible problems with disposed resources
- chance for overrun: the threading timer could start another thread while the previous is still being processed due to unexpected long work (I know, it can be prevented by stopping and restarting the timer)
However, the TPL solution always claims a dedicated thread which is not necessary while waiting for the next action (which is most of the time). I would like to use the proposed solution of Jeff to perform CPU bound cyclic work on the background because it only needs a threadpool thread when there is work to do which is better for scalability (especially when the interval period is big).
To achieve that, I would suggest 4 adaptions:
- Add
ConfigureAwait(false)
to the Task.Delay()
to execute the doWork
action on a thread pool thread, otherwise doWork
will be performed on the calling thread which is not the idea of parallelism
- Stick to the cancellation pattern by throwing a TaskCanceledException (still required ?)
- Forward the CancellationToken to
doWork
to enable it to cancel the task
- Add a parameter of type object to supply task state information (like a TPL task)
About point 2 I'm not sure, does async await still requires the TaskCanceledExecption or is it just best practice?
public static async Task Run(Action<object, CancellationToken> doWork, object taskState, TimeSpan period, CancellationToken cancellationToken)
{
do
{
await Task.Delay(period, cancellationToken).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
doWork(taskState, cancellationToken);
}
while (true);
}
Please give your comments to the proposed solution...
Update 2016-8-30
The above solution doesn't immediately call doWork()
but starts with await Task.Delay().ConfigureAwait(false)
to achieve the thread switch for doWork()
. The solution below overcomes this problem by wrapping the first doWork()
call in a Task.Run()
and await it.
Below is the improved async\await replacement for Threading.Timer
that performs cancelable cyclic work and is scalable (compared to the TPL solution) because it doesn’t occupy any thread while waiting for the next action.
Note that in contrary with the Timer, the waiting time (period
) is constant and not the cycle time; the cycle time is the sum of the waiting time and the duration of doWork()
which can vary.
public static async Task Run(Action<object, CancellationToken> doWork, object taskState, TimeSpan period, CancellationToken cancellationToken)
{
await Task.Run(() => doWork(taskState, cancellationToken), cancellationToken).ConfigureAwait(false);
do
{
await Task.Delay(period, cancellationToken).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
doWork(taskState, cancellationToken);
}
while (true);
}