I don't see any specific mention either on MSDN or here about how to accomplish this. The use case is somewhat obscure, but still valid I suspect.
var cancel = new CancellationTokenSource();
var task = Task.Factory.StartNew(() => { Task.Delay(1000, cancel.Token).Wait(); }, cancel.Token);
cancel.CancelAfter(100);
task.Wait();
The above code will attempt to cancel a task
that contains a detached child delay task after 100 milliseconds, and wait for the task
to complete which will generate an AggregateException
(due to the cancellation). The problem with this is that the task
becomes faulted instead of cancelled. This is expected behaviour because the delay task is not attached to the parent task
, even though both share the same cancellation token.
My question relates specifically to how you would go about attaching a Task.Delay
to a task that is already running. Is it possible to even do this if you had access to the parent task? If it isn't possible, or not possible without access to the parent task instance what is the proper way to handle this scenario?
The best work around I could come up with was to wrap the delay task's Wait
in a try/finally block, and explicitly attempt to bubble up the task cancellation.
try { Task.Delay(1000, cancel.Token).Wait(); } finally { cancel.Token.ThrowIfCancellationRequested(); }
While effective, it doesn't feel quite right, but I'm not sure if there is a better way to accomplish this. The desired outcome is that the parent task goes to Canceled
instead of Faulted
if cancellation occurs. So if the genesis of the cancellation occurs in a detached child task, the parent task should still transition to Canceled
.
NOTE: I left out async/await here on purpose, just because it doesn't appear to change the problem or the result. If that's not the case please provide an example.
So, a task is considered canceled when an OperationCanceledException
is thrown and uncaught inside it and its associated CancellationToken
is canceled.
In your case the exception being thrown is AggregateException
that contains a TaskCanceledException
(which is an OperationCanceledException
) instead of the TaskCanceledException
directly.
There's a simple way to fix that. Instead of synchronously blocking with Task.Wait
which wraps any exceptions in an AggregateException
wrapper you can use task.GatAwaiter().GetResult()
. That is what await
uses in async-await
. It throws the original exception and if there were multiple ones it throws the first:
var cancel = new CancellationTokenSource();
var task = Task.Factory.StartNew(() => { Task.Delay(1000, cancel.Token).GetAwaiter().GetResult(); }, cancel.Token);
cancel.CancelAfter(100);
task.Wait();
If you would have used async-await
you wouldn't have this issue as unlike Task.Wait
it throws the TaskCanceledException
itself:
var cancel = new CancellationTokenSource();
var task = Task.Run(() => Task.Delay(1000, cancel.Token), cancel.Token);
cancel.CancelAfter(100);
task.Wait();
I assume this is just an example. Real production code shouldn't resembles this as you're blocking synchronously on an asynchronous operation which in turn blocks synchronously on an asynchronous operation.