I've written these two wrappers around Tasks, to help with common scenarios in our code where methods are called repeatedly, even if the previous operation hasn't completed yet. There are two common scenarios:
- An operation is triggered several times by the user (e.g. clicking a checkbox filters a list by its value. User clicks 3 checkboxes in quick succession). In this case, every operation starts a possibly expensive server call, but I only care about the last one.
- A periodic operation is triggered by a timer every x seconds, but the operation sometimes takes more than x seconds. In this case, I don't want to start a new operation but to simply skip it, waiting for the next timer event to try again.
The solution I've used for both of these is similar:
public static void ExecuteAndCancelPreviousTask(ref Task previousTask, ref CancellationTokenSource cancellationTokenSource, Action action)
{
if (previousTask != null && !previousTask.IsCompleted)
cancellationTokenSource.Cancel();
cancellationTokenSource = new CancellationTokenSource();
previousTask = Task.Run(action, cancellationTokenSource.Token);
}
public static void ExecuteIfPreviousTaskComplete(ref Task previousTask, ref CancellationTokenSource cancellationTokenSource, Action action)
{
if (previousTask != null && !previousTask.IsCompleted)
return;
cancellationTokenSource = new CancellationTokenSource();
previousTask = Task.Run(action, cancellationTokenSource.Token);
}
In both cases, I receive the previously running operation (which can be null, the first time) and the CancellationTokenSource
attached to it. In the first case, I use the CTS to cancel the previous operation. In the second, I simply ignore it and skip running the action.
Of course, the Action
passed has to be CTS-aware, and check it for cancellation:
public void Timer_Elapsed()
{
TaskExtensions.ExecuteIfPreviousTaskComplete(ref _pollingTask, ref _pollingCancellationTokenSource, async () =>
{
var cts = _pollingCancellationTokenSource;
var pollingResults = await _service.Poll();
if (cts.IsCancellationRequested)
return;
UpdateFromResults(pollingResults);
});
}
My questions are:
Do you feel that these methods clearly express their purpose and usage?
I'm a bit leery of the two
ref
parameters. But since the methods have the update the task and its CTS, I could see no better way.I don't really have a need for the
action
to be run on a new thread. I mainly useTask.Run()
because I need, well, aTask
that can be monitored and cancelled. Should I instantiate the task some other way?The two methods are almost identical except for the second line of each, but I figured that their usage and intent are clearer when they're separate. Should I merge them? Should I keep them as two distinct public methods, but merge their implementation into a single private method? Or is this overkill for two and a half lines of code?
In the first case, should I wait, after Canceling the task, before creating a new one? Make sure it's ended?
Currently, I'm capturing the CancellationTokenSource at the beginning of my Action so that it doesn't get replaced when the
ref
is updated. Is this necessary? Or will the capture keep the original value?