I found a problem on a task cancellation pattern, and I would like to understand why should work in this way.
Consider this small program, where a secondary thread perform an async "long" task. In the mean time, the primary thread notifies for the cancellation.
The program is a very simplified version of a bigger one, which could have many concurrent threads doing a "long task". When the user ask to cancel, all the running task should be cancelled, hence the CancellationTokenSource collection.
class Program
{
static MyClass c = new MyClass();
static void Main(string[] args)
{
Console.WriteLine("program=" + Thread.CurrentThread.ManagedThreadId);
var t = new Thread(Worker);
t.Start();
Thread.Sleep(500);
c.Abort();
Console.WriteLine("Press any key...");
Console.ReadKey();
}
static void Worker()
{
Console.WriteLine("begin worker=" + Thread.CurrentThread.ManagedThreadId);
try
{
bool result = c.Invoker().Result;
Console.WriteLine("end worker=" + result);
}
catch (AggregateException)
{
Console.WriteLine("canceled=" + Thread.CurrentThread.ManagedThreadId);
}
}
class MyClass
{
private List<CancellationTokenSource> collection = new List<CancellationTokenSource>();
public async Task<bool> Invoker()
{
Console.WriteLine("begin invoker=" + Thread.CurrentThread.ManagedThreadId);
var cts = new CancellationTokenSource();
c.collection.Add(cts);
try
{
bool result = await c.MyTask(cts.Token);
return result;
}
finally
{
lock (c.collection)
{
Console.WriteLine("removing=" + Thread.CurrentThread.ManagedThreadId);
c.collection.RemoveAt(0);
}
Console.WriteLine("end invoker");
}
}
private async Task<bool> MyTask(CancellationToken token)
{
Console.WriteLine("begin task=" + Thread.CurrentThread.ManagedThreadId);
await Task.Delay(2000, token);
Console.WriteLine("end task");
return true;
}
public void Abort()
{
lock (this.collection)
{
Console.WriteLine("canceling=" + Thread.CurrentThread.ManagedThreadId);
foreach (var cts in collection) //exception here!
{
cts.Cancel();
}
//collection[0].Cancel();
};
}
}
}
Despite locking the collection access, the thread accessing it is the same as the one requesting the cancellation. This is, the collection is modified during an iteration, and an exception is raised.
For better clarity, you can comment out the whole "foreach" and uncomment the very last instruction, as follows:
public void Abort()
{
lock (this.collection)
{
Console.WriteLine("canceling=" + Thread.CurrentThread.ManagedThreadId);
//foreach (var cts in collection) //exception here!
//{
// cts.Cancel();
//}
collection[0].Cancel();
};
}
Doing so, there's no exception, and the program terminates gracefully. However, it's interesting to see the ID of the threads involved:
program=10
begin worker=11
begin invoker=11
begin task=11
canceling=10
removing=10
end invoker
Press any key...
canceled=11
Apparently, the "finally" body is run on the caller thread, but once off the "Invoker", the thread is the secondary.
Why the "finally" block is not executed in the secondary thread instead?