I have a UI button called Load. It spawns a thread, which in turn spawns a task. There is a wait on the task, and if it expires the task gets cancelled. The Load button is not disabled, and the user can click on it multiple times. Each time it's clicked the previous task should be cancelled.
I'm getting confused on how I should use the CancellationTokenSource and CancellationToken here. Below is the code. Can you please suggest how to use it and whether the below usage has any issues? No Async please as we are not there yet.
CancellationTokenSource _source = new CancellationTokenSource();
public void OnLoad()
{
//Does this cancel the previously spawned task?
_source.Cancel();
_source.Dispose();
_source = new CancellationTokenSource();
var activeToken = _source.Token;
//Do I need to do the above all the time or is there an efficient way?
Task.Factory.StartNew(() =>
{
var child = Task.Factory.StartNew(() =>
{
Thread.Sleep(TimeSpan.FromSeconds(20));
activeToken.ThrowIfCancellationRequested();
}, activeToken);
if (!child.Wait(TimeSpan.FromSeconds(5)))
{
_source.Cancel();
}
});
}
Note I need to cancel any previously spawned tasks, and every spawned task should have a timeout.
First, if you are using Visual Studio 2012+ you can add the Microsoft.Bcl.Async package to add support for async
and other advanced functionality to your .NET 4.0 project.
If you are using Visual Studio 2010, you can use the WithTimeout
extension method that comes with the ParallelExtensionsExtras library. The method wraps the original Task with a TaskCompletionSource and a timer that calls SetCancelled
if it expires.
The code is here but the actual method is simple:
/// <summary>Creates a new Task that mirrors the supplied task but that
/// will be canceled after the specified timeout.</summary>
/// <typeparam name="TResult">Specifies the type of data contained in the
/// task.</typeparam>
/// <param name="task">The task.</param>
/// <param name="timeout">The timeout.</param>
/// <returns>The new Task that may time out.</returns>
public static Task<TResult> WithTimeout<TResult>(this Task<TResult> task,
TimeSpan timeout)
{
var result = new TaskCompletionSource<TResult>(task.AsyncState);
var timer = new Timer(state =>
((TaskCompletionSource<TResult>)state).TrySetCanceled(),
result, timeout, TimeSpan.FromMilliseconds(-1));
task.ContinueWith(t =>
{
timer.Dispose();
result.TrySetFromTask(t);
}, TaskContinuationOptions.ExecuteSynchronously);
return result.Task;
}
You can use it right after creating your task:
var myTask=Task.Factory.StartNew(()=>{...})
.WithTimeout(TimeSpan.FromSeconds(20));
In general, you can create the behaviour you want by creating a TaskCompletionSource calling its SetResult, SetCancelled methods in response to the events or criteria you set.
This will do it:
private CancellationTokenSource _cancelTasks;
// this starts your process
private void DoStuff()
{
_cancelTasks = new CancellationTokenSource();
var task = new Task(() => { /* your actions here */ }, _cancelTasks.Token);
task.Start();
if (!task.Wait(5000)) _cancelTasks.Cancel();
}
There are a few mistakes in your code that make things confusing.
First, you're using Thread.Sleep instead of Task.Delay or some other timer-based method (I strongly recommend writing your own if you don't have access to Task.Delay). Sleep is a blocking wait and can't be conditioned on the cancel token. The result is precious thread pool threads being held hostage for multiple seconds, even if the operation is cancelled. This could result in the effects of later button presses being held up by earlier ones.
Second, at the end of the wait you're cancelling _source but this refers to the current _value of source not the value at the time the button was pressed. Earlier button presses will cancel later button press effects instead of their own.
Third, you're disposing the cancel token source on one thread while racing to cancel it on another. You're lucky you're not getting object disposed exceptions.
Fourth, it would be ideal to use async in this sort of situation. You mentioned you were only on .Net 4.0, though.
Fixing the first three things should make what's going on easier to reason about:
CancellationTokenSource _prevSource = new CancellationTokenSource();
public void OnButtonPress() {
var curSource = new CancellationTokenSource();
_prevSource.Cancel();
_prevSource = curSource;
MyCustomDelay(TimeSpan.FromSeconds(5), curSource.Token).ContinueWith(t => {
curSource.Cancel();
}, TaskContinuationOptions.OnlyOnRanToCompletion);
var r = MyCustomDelay(TimeSpan.FromSeconds(20), curSource.Token).ContinueWith(t => {
curSource.ThrowIfCancellationRequested();
}, TaskContinuationOptions.OnlyOnRanToCompletion);
// after 5 seconds the token r's delay is conditions on is cancelled
// so r is cancelled, due to the continuation specifying OnlyOnRanToCompletion
// the ThrowIfCancellationRequested line won't be executed
// although if we removed the cancel-after-5-seconds bit then it would be
}