2016-02-17 7 views
1

У меня есть несколько задач для выполнения. Каждая задача завершает свое выполнение в разное время. Некоторые из задач выполняют доступ к базе данных, некоторые из них просто делают некоторые вычисления. Мой код имеет следующую структуру:Задачи C# не отменены

var Canceller = new CancellationTokenSource(); 

List<Task<int>> tasks = new List<Task<int>>(); 

tasks.Add(new Task<int>(() => { Thread.Sleep(3000); Console.WriteLine("{0}: {1}", DateTime.Now, 3); return 3; }, Canceller.Token)); 
tasks.Add(new Task<int>(() => { Thread.Sleep(1000); Console.WriteLine("{0}: {1}", DateTime.Now, 1); return 1; }, Canceller.Token)); 
tasks.Add(new Task<int>(() => { Thread.Sleep(2000); Console.WriteLine("{0}: {1}", DateTime.Now, 2); return 2; }, Canceller.Token)); 
tasks.Add(new Task<int>(() => { Thread.Sleep(8000); Console.WriteLine("{0}: {1}", DateTime.Now, 8); return 8; }, Canceller.Token)); 
tasks.Add(new Task<int>(() => { Thread.Sleep(6000); Console.WriteLine("{0}: {1}", DateTime.Now, 6); return 6; }, Canceller.Token)); 

tasks.ForEach(x => x.Start()); 

bool Result = Task.WaitAll(tasks.Select(x => x).ToArray(), 3000); 

Console.WriteLine(Result); 

Canceller.Cancel(); 

tasks.ToList().ForEach(x => { x.Dispose(); }); // Exception here 
tasks.Clear(); 
tasks = null; 

Canceller.Dispose(); 
Canceller = null; 

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

При запуске кода Task.WaitAll параметр 3000 позволяет сначала выполнить 3 задачи, как ожидалось. Затем я получаю Result как false, потому что еще 2 задания не завершены. Затем я должен отменить эти две задачи. Если я попытаюсь их уничтожить, я получаю исключение: «Задачи в завершенном состоянии могут быть удалены».

Как я могу это достичь? После того, как я вызываю Cancel метод CancellationTokenSource, эти две задачи все еще выполняются. Что здесь не так?

+0

Разделить работу ('Sleep (8000)') в части ('Sleep (100) * 80') и проверка для ['IsCancelationRequest'] (https://msdn.microsoft.com/en-us/library/dd997396 (v = vs.110) .aspx) перед началом каждой части. – Sinatr

ответ

3

Во-первых, вы почти никогда не должны использовать Task.Start. Вместо этого используйте статический метод Task.Run.

Когда вы передаете CancellationToken в Task.Run или другие API-интерфейсы, которые создают задачи, это не позволяет сразу же отменить задачу, запросив отмену. Это только задает статус задачи Canceled, если код в задаче вызывает исключение OperationCanceledException. Пожалуйста, ознакомьтесь с разделом CancellationToken от this article.

Чтобы отменить задачу, код, с которым выполняется задание, должен сотрудничать с вами. Например, если код что-то делает в цикле, тогда этот код должен периодически проверяться, если требуется аннулирование, и выдавать исключение, если это так (или просто выйти из цикла, если вы не хотите, чтобы задача считалась отмененной). Существует метод в CancellationToken, называемый ThrowIfCancellationRequested, который делает именно это. Это, конечно, означает, что такой код должен иметь доступ к объекту CancellationToken. Вот почему у нас есть методы, которые принимают маркеры отмены.

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

Таким образом, отмена операции не является волшебной вещью, так как код, который выполняет задача, требует сотрудничества.

2

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

Мониторинг фишки с использованием синхронного Thread.Sleep может работать, если вы контролировать маркер после пробуждения от сна, это не будет не прервать любую текущую нить, которая в настоящее время находится в состоянии сна. Вместо этого я предоставляю альтернативу, используя Task.Delay. Это удобно, если вы хотите отслеживать токен, так как он позволяет передавать токен самой операции задержки.

Грубый эскиз асинхронного эквивалента может выглядеть следующим образом:

public async Task ExecuteAndTimeoutAsync() 
{ 
    var canceller = new CancellationTokenSource(); 
    var tasks = new[] 
    { 
     Task.Run(async() => 
     { 
      var delay = 2000; 
      await Task.Delay(delay, canceller.Token); 
      if (canceller.Token.IsCancellationRequested) 
      { 
       Console.WriteLine($"Operation with delay of {delay} cancelled"); 
       return -1; 
      } 
      Console.WriteLine("{0}: {1}", DateTime.Now, 3); 
      return 3; 
     }, canceller.Token), 
     Task.Run(async() => 
     { 
      var delay = 5000; 
      await Task.Delay(, canceller.Token); 
      if (canceller.Token.IsCancellationRequested) 
      { 
       Console.WriteLine($"Operation with delay of {delay} cancelled"); 
       return -1; 
      } 
      Console.WriteLine("{0}: {1}", DateTime.Now, 2); 
      return 2; 
     }, canceller.Token) 
    }; 

    await Task.Delay(3000); 
    canceller.Cancel(); 

    await Task.WhenAll(tasks); 
} 

При использовании асинхр не представляется возможным, рассмотреть вопрос о мониторинге на данной лексеме после с помощью Thread.Sleep, так что ваша нить знает, что вы на самом деле просили отмена.

Side Примечание:

  1. Использование Task.Run вместо new Task. Первый возвращает «горячую задачу», которая уже началась, нет необходимости повторять сбор и вызывать Start.
  2. Нет необходимости размещать Task. Используйте его, только если вы используете WaitHandle, выставленный Task, который вы здесь не используете.
  3. Предпочитаете использовать Task.WhenAll вместо Task.WaitAll.
  4. Выполняйте соглашения об именах .NET в своем коде.
+0

ваш пример технически корректен, но количество кода выглядит ужасно и требует функции –

+0

@StenPetrov Это так, но это быстро и грязно. Я не собираюсь повторять этот метод для него, потому что это похоже в основном как тест и выброс кода. Если это что-то, что собирается произвести, то определенная проверка кода в порядке. Но цель моего ответа - передать технический способ сделать это, а не правильный способ выполнения множественного выполнения задачи. –

+0

Замена 'Thread.Sleep' на' Task.Delay (..., токен) 'как чит. Можете ли вы заставить это работать с «Thread.Sleep» (что является синхронным методом, что не поддерживает отмену)? Я пытаюсь выяснить, имеет ли смысл мой комментарий к вопросу. – Sinatr

0

В классах задач отмена включает сотрудничество между делегатом пользователя, которое представляет собой операцию отмены и код, который запрашивал отмену. Успешное аннулирование включает запрашивающий код, вызывающий метод CancellationTokenSource.Cancel(), и пользовательский делегат, своевременно завершающий операцию. Вы можете завершить операцию, используя один из следующих вариантов:

  • Просто вернувшись от делегата. Во многих сценариях этого достаточно; однако экземпляр задачи, который отменяется таким образом, переходит в состояние TaskStatus.RanToCompletion, а не в состояние TaskStatus.Canceled.

  • Отбрасывая OperationCanceledException и передавая ему токен, по которому была запрошена отмена. Предпочтительный способ сделать это - использовать метод ThrowIfCancellationRequested(). Задача, которая отменяется таким образом, переходит в состояние отмены, которое вызывающий код может использовать для проверки того, что задача ответила на запрос аннулирования.

Таким образом, вы должны слушать сигнал отмены в ваших задач:

var Canceller = new CancellationTokenSource(); 
var token = Canceller.Token; 

List<Task<int>> tasks = new List<Task<int>>(); 

tasks.Add(new Task<int>(() => { Thread.Sleep(3000); token.ThrowIfCancellationRequested(); Console.WriteLine("{0}: {1}", DateTime.Now, 3); return 3; }, token)); 
tasks.Add(new Task<int>(() => { Thread.Sleep(1000); token.ThrowIfCancellationRequested(); Console.WriteLine("{0}: {1}", DateTime.Now, 1); return 1; }, token)); 
tasks.Add(new Task<int>(() => { Thread.Sleep(2000); token.ThrowIfCancellationRequested(); Console.WriteLine("{0}: {1}", DateTime.Now, 2); return 2; }, token)); 
tasks.Add(new Task<int>(() => { Thread.Sleep(8000); token.ThrowIfCancellationRequested(); Console.WriteLine("{0}: {1}", DateTime.Now, 8); return 8; }, token)); 
tasks.Add(new Task<int>(() => { Thread.Sleep(6000); token.ThrowIfCancellationRequested(); Console.WriteLine("{0}: {1}", DateTime.Now, 6); return 6; }, token)); 

tasks.ForEach(x => x.Start()); 

bool Result = Task.WaitAll(tasks.Select(x => x).ToArray(), 3000); 

Console.WriteLine(Result); 

Canceller.Cancel(); 

try 
{ 
    Task.WaitAll(tasks.ToArray()); 
} 
catch (AggregateException ex) 
{ 
    if (!(ex.InnerException is TaskCanceledException)) 
     throw ex.InnerException; 
} 

tasks.ToList().ForEach(x => { x.Dispose(); }); 
tasks.Clear(); 
tasks = null; 

Canceller.Dispose(); 
Canceller = null;