I created a small wrapper around CancellationToken
and CancellationTokenSource
. The problem I have is that the CancelAsync
method of CancellationHelper
doesn't work as expected.
I'm experiencing the problem with the ItShouldThrowAExceptionButStallsInstead
method. To cancel the running task, it calls await coordinator.CancelAsync();
, but the task is not cancelled actually and doesn't throw an exception on task.Wait
ItWorksWellAndThrowsException
seems to be working well and it uses coordinator.Cancel
, which is not an async method at all.
The question why is the task is not cancelled when I call CancellationTokenSource
's Cancel method in async method?
Don't let the waitHandle
confuse you, it's only for not letting the task finish early.
Let the code speak for itself:
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace TestCancellation
{
class Program
{
static void Main(string[] args)
{
ItWorksWellAndThrowsException();
//ItShouldThrowAExceptionButStallsInstead();
}
private static void ItShouldThrowAExceptionButStallsInstead()
{
Task.Run(async () =>
{
var coordinator = new CancellationHelper();
var waitHandle = new ManualResetEvent(false);
var task = Task.Run(() =>
{
waitHandle.WaitOne();
//this works well though - it throws
//coordinator.ThrowIfCancellationRequested();
}, coordinator.Token);
await coordinator.CancelAsync();
//waitHandle.Set(); -- with or without this it will throw
task.Wait();
}).Wait();
}
private static void ItWorksWellAndThrowsException()
{
Task.Run(() =>
{
var coordinator = new CancellationHelper();
var waitHandle = new ManualResetEvent(false);
var task = Task.Run(() => { waitHandle.WaitOne(); }, coordinator.Token);
coordinator.Cancel();
task.Wait();
}).Wait();
}
}
public class CancellationHelper
{
private CancellationTokenSource cancellationTokenSource;
private readonly List<Task> tasksToAwait;
public CancellationHelper()
{
cancellationTokenSource = new CancellationTokenSource();
tasksToAwait = new List<Task>();
}
public CancellationToken Token
{
get { return cancellationTokenSource.Token; }
}
public void AwaitOnCancellation(Task task)
{
if (task == null) return;
tasksToAwait.Add(task);
}
public void Reset()
{
tasksToAwait.Clear();
cancellationTokenSource = new CancellationTokenSource();
}
public void ThrowIfCancellationRequested()
{
cancellationTokenSource.Token.ThrowIfCancellationRequested();
}
public void Cancel()
{
cancellationTokenSource.Cancel();
Task.WaitAll(tasksToAwait.ToArray());
}
public async Task CancelAsync()
{
cancellationTokenSource.Cancel();
try
{
await Task.WhenAll(tasksToAwait.ToArray());
}
catch (AggregateException ex)
{
ex.Handle(p => p is OperationCanceledException);
}
}
}
}
Cancellation in .NET is cooperative.
That means that the one holding the
CancellationTokenSource
signals cancellation and the one holding theCancellationToken
needs to check whether cancellation was signaled (either by polling theCancellationToken
or by registering a delegate to run when it is signaled).In your
Task.Run
you use theCancellationToken
as a parameter, but you don't check it inside the task itself so the task will only be cancelled if the token was signaled before the task had to a chance to start.To cancel the task while it's running you need to check the
CancellationToken
:In your case you block on a
ManualResetEvent
so you wouldn't be able to check theCancellationToken
. You can register a delegate to theCancellationToken
that frees up the reset event: