Я обнаружил проблему в шаблоне отмены задачи, и я хотел бы понять, почему это должно работать таким образом.
Рассмотрим эту небольшую программу, в которой вторичный поток выполняет асинхронную «длинную» задачу. Тем временем основной поток уведомляет об отмене.
Программа представляет собой очень упрощенную версию более крупной программы, в которой может быть много параллельных потоков, выполняющих «длинную задачу». Когда пользователь просит отменить, все запущенные задачи должны быть отменены, следовательно, коллекция CancellationTokenSource.
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();
};
}
}
}
Несмотря на блокировку доступа к коллекции, поток, обращающийся к ней, совпадает с потоком, запрашивающим отмену. То есть коллекция изменяется во время итерации, и возникает исключение.
Для большей ясности можно закомментировать весь foreach и раскомментировать самую последнюю инструкцию следующим образом:
public void Abort()
{
lock (this.collection)
{
Console.WriteLine("canceling=" + Thread.CurrentThread.ManagedThreadId);
//foreach (var cts in collection) //exception here!
//{
// cts.Cancel();
//}
collection[0].Cancel();
};
}
При этом исключений нет, и программа корректно завершается. Однако интересно увидеть идентификаторы задействованных потоков:
program=10
begin worker=11
begin invoker=11
begin task=11
canceling=10
removing=10
end invoker
Press any key...
canceled=11
По-видимому, тело «наконец» запускается в вызывающем потоке, но после выхода из «Инвокера» поток становится вторичным.
Почему блок «наконец» вместо этого не выполняется во вторичном потоке?