2014-02-03 5 views
4

Я нашел проблему с шаблоном аннулирования задачи, и я хотел бы понять, почему это должно работать таким образом.Почему отмена задачи происходит в потоке вызывающего абонента?

Рассмотрите эту небольшую программу, где вторичный поток выполняет асинхронную «длинную» задачу. В то же время основной поток уведомляет об аннулировании.

Программа является очень упрощенной версией более крупной, которая может иметь много параллельных потоков, выполняющих «длинную задачу». Когда пользователь попросит отменить, вся работающая задача должна быть отменена, следовательно, коллекция 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(); 
      }; 
     } 

    } 
} 

Несмотря на блокировку доступа к коллекции, доступ к ней осуществляется так же, как и запрос на отмену. Это значит, что коллекция изменяется во время итерации, и возникает исключение.

Для большей ясности, вы можете закомментировать весь «Еогеасп» и раскомментируйте самую последнюю команду, следующим образом:

 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 

Судя по всему, «наконец» телу работать на потоке вызывающего абонента, но когда-то от «заклинателя», поток является вторичным.

Почему блок «finally» вместо этого не выполняется во вторичном потоке?

ответ

4

В какой поток запускается задача, это деталь реализации. Один из них можно было бы только приклеить, если вы используете планировщик задач, который знает, как запускать код в определенном потоке. Как TaskScheduler.FromCurrentSynchronizationContext(). Это никогда не будет работать в консольном режиме, так как в нем нет.

Так что до реализации класса Task выяснить, какой поток использовать. И он будет искать возможность для не требует переключения контекста потока, это дорого. Если у него есть выбор между запуском потока threadpool для выполнения кода и ожиданием его завершения и выполнения кода напрямую, он всегда выбирает последний выбор, он превосходит.

Он найден в вашем коде, вы вызвали метод Abort() в своем основном потоке. Который, через множество слоев в кабинете класса задач (смотрите окно окна вызова), выяснил, как вызвать , наконец, блок в том же потоке. Разумеется, это хорошая вещь. И вы должны ожидать, что в вашем потоке больше нет ничего, что могло бы быть использовано для выполнения кода задачи.

Сравните с использованием функции CancelAfter(), теперь ваша нить не подходит для выполнения блока finally, и вы увидите, что блок finally выполняется в потоке TP.

+0

Также интересно отметить, что аннулирование передается с 'await Task.Delay (3000, токен);': catch и rethrow 'Exception' в MyTask, а также catch и rethrow' Exception' в Invoker - и вы увидите что это 'TaskCanceledException' в MyTask (поток, который мы назвали Cancel() from),' TaskCanceledException' в Invoker (также поток, который мы назвали Cancel()), и, наконец, он становится 'AggregateException', но в потоке, который ждал Результат –

+0

'TaskScheduler.FromCurrentSynchronizationContext(). Это никогда не будет работать в консольном режиме, так как оно не имеет его. «Не« никогда ». Вы можете вручную создать контекст синхронизации и установить его в текущем в консольном приложении. «Обычно» может быть уместно здесь, а не «никогда». – Servy

0

Похоже, что после того, как вы вызвали Cancel() на первый дочерний поток, продолжение await не может возобновить возврат на этот поток и вместо этого выполняется в потоке вызывающего/родительского потока.Если добавить catch сразу после вызова на нерест второй дочерний поток, вы можете увидеть код, выполняемый родительской нити после TaskCancelationException,

try 
{ 
    bool result = await c.MyTask(cts.Token); 
    return result; 
} 
catch (Exception exception) 
{ 
    Console.WriteLine("catch invoker exception=" + exception.GetType()); 
    Console.WriteLine("catch invoker=" + Thread.CurrentThread.ManagedThreadId); 
    return true; 
} 

который производит,

program=10 
begin worker=11 
begin invoker=11 
begin task=11 
canceling=10 
catch invoker exception=TaskCanceledException 
catch invoker=10  <-- parent thread resuming on child cancellation 
removing=10 

Причина он выполняется на родительском потоке, может быть деталью реализации из-за причин производительности для нереста нового потока, чтобы возобновить выполнение (что объяснил Ханс Пассант); Аналогично, если ребенок нить никогда не отменяется (закомментировать c.Abort();), то await выполнение будет возобновлено в обоих случаях на ребенка нить, а не родителя,

program=10 
begin worker=11 <-- first child thread 
begin invoker=11 
begin task=11 
Press any key... 
end task=12  <-- second child thread resuming on 'await Task.Delay' 
removing=12  <-- second child thread resuming on 'await c.MyTask(cts.Token)' 
end invoker=12 
end worker=True  
end worker=11  <-- back to the first child thread 

Где thread 11, который уже вернулся обратно в вызывающую метод (обратно в Worker) может оказаться более дорогостоящим, чтобы переключить контекст потока, чтобы возобновить на MyTask, тогда как thread 12 (предполагаемый второй ребенок) только что стал доступен для продолжения, но только до конца методов Invoker, где thread 11 в том месте, где оно было первоначально приостановлено.