Is there a proper way to cancel an asynchronous task? - c#

I encountered a problem how to properly cancel an asynchronous task.
Here is some draft.
My entry point runs two asynchronous task. The first task does some 'long' work and the second one cancels it.
Entry point:
private static void Main()
{
var ctc = new CancellationTokenSource();
var cancellable = ExecuteLongCancellableMethod(ctc.Token);
var cancelationTask = Task.Run(() =>
{
Thread.Sleep(2000);
Console.WriteLine("[Before cancellation]");
ctc.Cancel();
});
try
{
Task.WaitAll(cancellable, cancelationTask);
}
catch (Exception e)
{
Console.WriteLine($"An exception occurred with type {e.GetType().Name}");
}
}
Method that returns cancel-able task:
private static Task ExecuteLongCancellableMethod(CancellationToken token)
{
return Task.Run(() =>
{
token.ThrowIfCancellationRequested();
Console.WriteLine("1st");
Thread.Sleep(1000);
Console.WriteLine("2nd");
Thread.Sleep(1000);
Console.WriteLine("3rd");
Thread.Sleep(1000);
Console.WriteLine("4th");
Thread.Sleep(1000);
Console.WriteLine("[Completed]");
}, token);
}
My purpose is to stop writing '1st','2nd','3rd' immediately after cancellation is called. But I get following results:
1st
2nd
3rd
[Before cancellation]
4th
[Completed]
For obvious reason I didn't get an exception that throws when cancellation is requested. So I tried to rewrite method as following:
private static Task ExecuteLongCancellableAdvancedMethod(CancellationToken token)
{
return Task.Run(() =>
{
var actions = new List<Action>
{
() => Console.WriteLine("1st"),
() => Console.WriteLine("2nd"),
() => Console.WriteLine("3rd"),
() => Console.WriteLine("4th"),
() => Console.WriteLine("[Completed]")
};
foreach (var action in actions)
{
token.ThrowIfCancellationRequested();
action.Invoke();
Thread.Sleep(1000);
}
}, token);
}
And now I got what I want:
1st
2nd
[Before cancellation]
3rd
An exception occurred with type AggregateException
But I guess creating of a collection of Action delegates and looping through it is not the most convenient way to deal with my problem.
So what's the proper way to do it? And why do I need to pass my cancellation token into Task.Run method as the second argument?

The Task won't cancel its self, it is up you you to detect the cancellation request and cleanly abort your work. That's what token.ThrowIfCancellationRequested(); does.
You should place those checks throughout your code, in places where execution can be cleanly stopped, or rolled back to a safe state.
In your second example, you call it once per iteration of the loop, and it works fine. The first example only calls it once, at the very start. If the token hasn't been canceled by that point, the task will run to completion, just like you are seeing.
If you changed it to look like this, you would also see the results you expect.
return Task.Run(() =>
{
token.ThrowIfCancellationRequested();
Console.WriteLine("1st");
Thread.Sleep(1000);
token.ThrowIfCancellationRequested();
Console.WriteLine("2nd");
Thread.Sleep(1000);
token.ThrowIfCancellationRequested();
Console.WriteLine("3rd");
Thread.Sleep(1000);
token.ThrowIfCancellationRequested();
Console.WriteLine("4th");
Thread.Sleep(1000);
Console.WriteLine("[Completed]");
}, token);

Related

Passing cancellation token to Task.Run seems to have no effect [duplicate]

This question already has an answer here:
Faulted vs Canceled task status after CancellationToken.ThrowIfCancellationRequested
(1 answer)
Closed last month.
According to this and this, passing a cancellation token to a task constructor, or Task.Run, will cause the task to be associated with said token, causing the task to transition to Canceled instead of Faulted if a cancellation exception occurs.
I've been fiddling with these examples for a while, and I can't see any benefits other than preventing a cancelled task to start.
Changing the code on this MSDN example from
tc = Task.Run(() => DoSomeWork(i, token), token);
to
tc = Task.Run(() => DoSomeWork(i, token));
produced the exact same output:
This code also results in two cancelled state tasks with the same exceptions thrown:
var token = cts.Token;
var t1 = Task.Run(() =>
{
while (true)
{
Thread.Sleep(1000);
token.ThrowIfCancellationRequested();
};
});
var t2 = Task.Run(() =>
{
while (true)
{
Thread.Sleep(1000);
token.ThrowIfCancellationRequested();
};
}, token);
Console.ReadKey();
try
{
cts.Cancel();
Task.WaitAll(t1, t2);
}
catch(Exception e)
{
if (e is AggregateException)
{
foreach (var ex in (e as AggregateException).InnerExceptions)
{
Console.WriteLine(e.Message);
}
}
else
Console.WriteLine(e.Message);
}
Console.WriteLine($"without token: { t1.Status }");
Console.WriteLine($"with token: { t2.Status }");
Console.WriteLine("Done.");
Apparently, throwing OperationCanceledException from within the task is enough to make it transition to Canceled instead of Faulted. So my question is: is there a reason for passing the token to the task other than preventing a cancelled task to run?
Is there a reason for passing the token to the task other than preventing a cancelled task to run?
In this particular case, No. Past the point in time that the task has started running, the token has no effect to the outcome.
The Task.Run method has many overloads. This case is peculiar because of the infinite while loop.
var t1 = Task.Run(() =>
{
while (true)
{
Thread.Sleep(1000);
token.ThrowIfCancellationRequested();
};
});
The compiler has to choose between these two overloads:
public static Task Run(Action action);
public static Task Run(Func<Task> function);
...and for a reason explained in this question it chooses the later. Here is the implementation of this overload:
public static Task Run(Func<Task?> function, CancellationToken cancellationToken)
{
if (function == null) ThrowHelper.ThrowArgumentNullException(ExceptionArgument.function);
// Short-circuit if we are given a pre-canceled token
if (cancellationToken.IsCancellationRequested)
return Task.FromCanceled(cancellationToken);
// Kick off initial Task, which will call the user-supplied function and yield a Task.
Task<Task?> task1 = Task<Task?>.Factory.StartNew(function, cancellationToken,
TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
// Create a promise-style Task to be used as a proxy for the operation
// Set lookForOce == true so that unwrap logic can be on the lookout for OCEs thrown
// as faults from task1, to support in-delegate cancellation.
UnwrapPromise<VoidTaskResult> promise = new UnwrapPromise<VoidTaskResult>(task1,
lookForOce: true);
return promise;
}
The important detail is the lookForOce: true. Let's look inside the UnwrapPromise class:
// "Should we check for OperationCanceledExceptions on the outer task and interpret them
// as proxy cancellation?"
// Unwrap() sets this to false, Run() sets it to true.
private readonly bool _lookForOce;
..and at another point below:
case TaskStatus.Faulted:
List<ExceptionDispatchInfo> edis = task.GetExceptionDispatchInfos();
ExceptionDispatchInfo oceEdi;
if (lookForOce && edis.Count > 0 &&
(oceEdi = edis[0]) != null &&
oceEdi.SourceException is OperationCanceledException oce)
{
result = TrySetCanceled(oce.CancellationToken, oceEdi);
}
else
{
result = TrySetException(edis);
}
break;
So although the internally created Task<Task?> task1 ends up in a Faulted state, its unwrapped version ends up as Canceled, because the type of the exception is
OperationCanceledException (abbreviated as oce in the code).
That's a quite convoluted journey in the history of TPL, with methods introduced at different times and frameworks, in order to serve different purposes. The end result is a little bit of inconsistency, or nuanced behavior if you prefer to say it so. A relevant article that you might find interesting is this: Task.Run vs Task.Factory.StartNew by Stephen Toub.

Switch new Task(()=>{ }) for Func<Task>

In an answer to one of my other questions, I was told that use of new Task(() => { }) is not something that is a normal use case. I was advised to use Func<Task> instead. I have tried to make that work, but I can't seem to figure it out. (Rather than drag it out in the comments, I am asking a separate question here.)
My specific scenario is that I need the Task to not start right when it is declared and to be able to wait for it later.
Here is a LinqPad example using new Task(() => { }). NOTE: This works perfectly! (Except that it uses new Task.)
static async void Main(string[] args)
{
// Line that I need to swap to a Func<Task> somehow.
// note that this is "cold" not started task
Task startupDone = new Task(() => { });
var runTask = DoStuff(() =>
{
//+++ This is where we want to task to "start"
startupDone.Start();
});
//+++ Here we wait for the task to possibly start and finish. Or timeout.
// Note that this times out at 1000ms even if "blocking = 10000" below.
var didStartup = startupDone.Wait(1000);
Console.WriteLine(!didStartup ? "Startup Timed Out" : "Startup Finished");
await runTask;
Console.Read();
}
public static async Task DoStuff(Action action)
{
// Swap to 1000 to simulate starting up blocking
var blocking = 1; //1000;
await Task.Delay(500 + blocking);
action();
// Do the rest of the stuff...
await Task.Delay(1000);
}
I tried swapping the second line with:
Func<Task> startupDone = new Func<Task>(async () => { });
But then the lines below the comments with +++ in them don't work right.
I swapped the startupDone.Start() with startupDone.Invoke().
But startupDone.Wait needs the task. Which is only returned in the lambda. I am not sure how to get access to the task outside the lambda so I can Wait for it.
How can use a Func<Task> and start it in one part of my code and do a Wait for it in another part of my code? (Like I can with new Task(() => { })).
The code you posted cannot be refactored to make use of a Func<Task> instead of a cold task, because the method that needs to await the task (the Main method) is not the same method that controls the creation/starting of the task (the lambda parameter of the DoStuff method). This could make the use of the Task constructor legitimate in this case, depending on whether the design decision to delegate the starting of the task to a lambda is justified. In this particular example the startupDone is used as a synchronization primitive, to signal that a condition has been met and the program can continue. This could be achieved equally well by using a specialized synchronization primitive, like for example a SemaphoreSlim:
static async Task Main(string[] args)
{
var startupSemaphore = new SemaphoreSlim(0);
Task runTask = RunAsync(startupSemaphore);
bool startupFinished = await startupSemaphore.WaitAsync(1000);
Console.WriteLine(startupFinished ? "Startup Finished" : "Startup Timed Out");
await runTask;
}
public static async Task RunAsync(SemaphoreSlim startupSemaphore)
{
await Task.Delay(500);
startupSemaphore.Release(); // Signal that the startup is done
await Task.Delay(1000);
}
In my opinion using a SemaphoreSlim is more meaningful in this case, and makes the intent of the code clearer. It also allows to await asynchronously the signal with a timeout WaitAsync(Int32), which is not something that you get from a Task out of the box (it is doable though).
Using cold tasks may be tempting in some cases, but when you revisit your code after a month or two you'll find yourself confused, because of how rare and unexpected is to have to deal with tasks that may or may have not been started yet.
I always try my hardest to never have blocking behavior when dealing with anything async or any type that represents potential async behavior such as Task. You can slightly modify your DoStuff to facilitate waiting on your Action.
static async void Main(string[] args)
{
Func<CancellationToken,Task> startupTask = async(token)=>
{
Console.WriteLine("Waiting");
await Task.Delay(3000, token);
Console.WriteLine("Completed");
};
using var source = new CancellationTokenSource(2000);
var runTask = DoStuff(() => startupTask(source.Token), source.Token);
var didStartup = await runTask;
Console.WriteLine(!didStartup ? "Startup Timed Out" : "Startup Finished");
Console.Read();
}
public static async Task<bool> DoStuff(Func<Task> action, CancellationToken token)
{
var blocking = 10000;
try
{
await Task.Delay(500 + blocking, token);
await action();
}
catch(TaskCanceledException ex)
{
return false;
}
await Task.Delay(1000);
return true;
}
First, the type of your "do this later" object is going to become Func<Task>. Then, when the task is started (by invoking the function), you get back a Task that represents the operation:
static async void Main(string[] args)
{
Func<Task> startupDoneDelegate = async () => { };
Task startupDoneTask = null;
var runTask = await DoStuff(() =>
{
startupDoneTask = startupDoneDelegate();
});
var didStartup = startupDoneTask.Wait(1000);
Console.WriteLine(!didStartup ? "Startup Timed Out" : "Startup Finished");
}

Can cancellation token be used at tasks method within?

I've just started working with tasks and I've come to some things I don't quite understand about calling methods within the task. I have started a new task like this:
var ts = new CancellationTokenSource();
var token = ts.Token;
Task.Run(() => Control(), token);
void Control()
{
while(!token.IsCancellationRequested)
{
token.ThrowIfCancellationRequested();
switch(ENUM)
{
case SOMETHING:
StartSomething();
break;
}
Task.Delay(50, token).wait();
}
}
Now I don't understand the behavior of StartSomething() once token has been cancelled. What if StartSomething() as well contains a while loop, can I as well use?
!token.IsCancellationRequested
and
token.ThrowIfCancellationRequested();
As well, if the Cancellation exception is being thrown inside that StartSomething() loop, will it instantly cancel task?
Yes, you can easily pass the same token onto StartSomething and exceptions from it will bubble up to Control and cancel the task. If you don't then it will keep running even if the CancellationTokenwas cancelled until it returns control toControl` that observes the token:
void StartSomething(CancellationToken token)
{
while (true)
{
token.ThrowIfCancellationRequested(); // Will cancel the task.
// ...
}
}
Keep in mind though that token.ThrowIfCancellationRequested() will raise exception and the task will be canceled while !token.IsCancellationRequested will simply complete the task without marking it as canceled.

NotOnRanToCompletion Continuation doesn't run when parent task is cancelled

I'm trying to test a scenario where I have a task that can be cancelled, and a continuation that should be running if the antecedent task doesn't complete. A sample of the code is like this:
static void Main(string[] args)
{
var source = new CancellationTokenSource();
var task = Task.Factory.StartNew(() =>
{
while (true)
{
source.Token.ThrowIfCancellationRequested();
}
}, source.Token);
var continuation = task.ContinueWith(t =>
{
Console.WriteLine("Continuation");
if (t.Status == TaskStatus.Faulted)
{
Console.WriteLine("Antecedent Faulted: " + t.Exception.Message);
}
}, source.Token, TaskContinuationOptions.NotOnRanToCompletion | TaskContinuationOptions.AttachedToParent, TaskScheduler.Current);
var cancellation = Task.Factory.StartNew(() =>
{
Thread.Sleep(1000);
source.Cancel();
});
try
{
continuation.Wait();
}
catch (AggregateException)
{
Console.WriteLine("AggregateException");
}
Console.WriteLine("Done");
while (!Console.KeyAvailable) { }
}
The output of this program is:
AggregateException
Done
To a certain extent, I get it. The primary task was cancelled, which ends up throwing a TaskCanceledException which gets wrapped in the AggregateException. The question is, is this expected behaviour? If so, what use is TaskStatus.Faulted in a continuation, if that continuation isn't getting executed? I even set a looping check for ConsoleKeyAvailable just in case the continuation does get run as well as the AggregateException getting thrown.
I believe the reason this is occurring is you are passing source.Token as the CancellationToken for the call to task.ContinueWith. Despite passing NotOnRanToCompletion as the continuation options, the fact that the token is cancelled before the continuation task ever starts means the scheduler can immediately transition it to the canceled state without actually running it.

Cancellation Token source and nested tasks

I have a doubt with cancellation token source which I am using as shown in the below code:
void Process()
{
//for the sake of simplicity I am taking 1, in original implementation it is more than 1
var cancellationToken = _cancellationTokenSource.Token;
Task[] tArray = new Task[1];
tArray[0] = Task.Factory.StartNew(() =>
{
cancellationToken.ThrowIfCancellationRequested();
//do some work here
MainTaskRoutine();
}, cancellationToken);
try
{
Task.WaitAll(tArray);
}
catch (Exception ex)
{
//do error handling here
}
}
void MainTaskRoutine()
{
//for the sake of simplicity I am taking 1, in original implementation it is more than 1
//this method shows that a nested task is created
var cancellationToken = _cancellationTokenSource.Token;
Task[] tArray = new Task[1];
tArray[0] = Task.Factory.StartNew(() =>
{
cancellationToken.ThrowIfCancellationRequested();
//do some work here
}, cancellationToken);
try
{
Task.WaitAll(tArray);
}
catch (Exception ex)
{
//do error handling here
}
}
Edit: further elaboration
End objective is: when a user cancels the operation, all the immediate pending tasks(either children or grand children) should cancel.
Scenario:
As per above code:
1. I first check whether user has asked for cancellation
2. If user has not asked for cancellation then only continue with the task (Please see Process method).
sample code shows only one task here but actually there can be three or more
Lets say that CPU started processing Task1 while other tasks are still in the Task queue waiting for some CPU to come and execute them.
User requests cancellation: Task 2,3 in Process method are immediately cancelled, but Task 1 will continue to work since it is already undergoing processing.
In Task 1 it calls method MainTaskRoutine, which in turn creates more tasks.
In the function of MainTaskRoutine I have written: cancellationToken.ThrowIfCancellationRequested();
So the question is: is it correct way of using CancellationTokenSource as it is dependent on Task.WaitAll()?
[EDITED] As you use an array in your code, I assume there could be multiple tasks, not just one. I also assume that within each task that you're starting from Process you want to do some CPU-bound work first (//do some work here), and then run MainTaskRoutine.
How you handle task cancellation exceptions is determined by your project design workflow. E.g., you could do it inside Process method, or from where you call Process. If your only concern is to remove Task objects from the array where you keep track of the pending tasks, this can be done using Task.ContinueWith. The continuation will be executed regardless of the task's completion status (Cancelled, Faulted or RanToCompletion):
Task Process(CancellationToken cancellationToken)
{
var tArray = new List<Task>();
var tArrayLock = new Object();
var task = Task.Run(() =>
{
cancellationToken.ThrowIfCancellationRequested();
//do some work here
return MainTaskRoutine(cancellationToken);
}, cancellationToken);
// add the task to the array,
// use lock as we may remove tasks from this array on a different thread
lock (tArrayLock)
tArray.Add(task);
task.ContinueWith((antecedentTask) =>
{
if (antecedentTask.IsCanceled || antecedentTask.IsFaulted)
{
// handle cancellation or exception inside the task
// ...
}
// remove task from the array,
// could be on a different thread from the Process's thread, use lock
lock (tArrayLock)
tArray.Remove(antecedentTask);
}, TaskContinuationOptions.ExecuteSynchronously);
// add more tasks like the above
// ...
// Return aggregated task
Task[] allTasks = null;
lock (tArrayLock)
allTasks = tArray.ToArray();
return Task.WhenAll(allTasks);
}
Your MainTaskRoutine can be structured in exactly the same way as Process, and have the same method signature (return a Task).
Then you may want to perform a blocking wait on the aggregated task returned by Process, or handle its completion asynchronously, e.g:
// handle the completion asynchronously with a blocking wait
void RunProcessSync()
{
try
{
Process(_cancellationTokenSource.Token).Wait();
MessageBox.Show("Process complete");
}
catch (Exception e)
{
MessageBox.Show("Process cancelled (or faulted): " + e.Message);
}
}
// handle the completion asynchronously using ContinueWith
Task RunProcessAync()
{
return Process(_cancellationTokenSource.Token).ContinueWith((task) =>
{
// check task.Status here
MessageBox.Show("Process complete (or cancelled, or faulted)");
}, TaskScheduler.FromCurrentSynchronizationContext());
}
// handle the completion asynchronously with async/await
async Task RunProcessAync()
{
try
{
await Process(_cancellationTokenSource.Token);
MessageBox.Show("Process complete");
}
catch (Exception e)
{
MessageBox.Show("Process cancelled (or faulted): " + e.Message);
}
}
After doing some research I found this link.
The code now looks like this:
see the usage of CancellationTokenSource.CreateLinkedTokenSource in below code
void Process()
{
//for the sake of simplicity I am taking 1, in original implementation it is more than 1
var cancellationToken = _cancellationTokenSource.Token;
Task[] tArray = new Task[1];
tArray[0] = Task.Factory.StartNew(() =>
{
cancellationToken.ThrowIfCancellationRequested();
//do some work here
MainTaskRoutine(cancellationToken);
}, cancellationToken);
try
{
Task.WaitAll(tArray);
}
catch (Exception ex)
{
//do error handling here
}
}
void MainTaskRoutine(CancellationToken cancellationToken)
{
//for the sake of simplicity I am taking 1, in original implementation it is more than 1
//this method shows that a nested task is created
using (var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
{
var cancelToken = cancellationTokenSource.Token;
Task[] tArray = new Task[1];
tArray[0] = Task.Factory.StartNew(() =>
{
cancelToken.ThrowIfCancellationRequested();
//do some work here
}, cancelToken);
try
{
Task.WaitAll(tArray);
}
catch (Exception ex)
{
//do error handling here
}
}
}
Note: I haven't used it, but I will let You know once it is done :)

Categories

Resources