2012-05-21 4 views
6

Все, вот вопрос о дизайне/лучших практиках сложного случая отмены Task: s в C#. Как реализовать отмену общей задачи?Как реализовать отмену совместной задачи: s в C#

В качестве минимального примера допустим следующее: у нас есть долго работающая, совместная операция «Работа». Он принимает маркер отмены в качестве аргумента и выдает, если он был отменен. Он работает в определенном состоянии приложения и возвращает значение. Его результат независимо требуется двумя компонентами пользовательского интерфейса.

Пока состояние приложения не изменяется, значение функции Работы должно быть кэшировано, и если одно вычисление продолжается, новый запрос не должен запускать второй расчет, а скорее начнет ждать результата.

Любой из компонентов пользовательского интерфейса должен иметь возможность отменить свою задачу, не затрагивая задачу других компонентов пользовательского интерфейса.

Вы со мной до сих пор?

Вышеупомянутое может быть выполнено путем ввода кэша задач, который обертывает реальную Рабочую задачу в ресурсах TaskCompletion, чья задача: s затем возвращается компонентам пользовательского интерфейса. Если компонент пользовательского интерфейса отменяет его «Задача», он отменяет задачу TaskCompletionSource, а не основную задачу. Это все хорошо. Компоненты пользовательского интерфейса создают источник CancellationSource, а запрос на отмену - это нормальный дизайн сверху вниз, а внизу - работающий TaskCompletionSource Task.

Теперь, к настоящей проблеме. Что делать, когда изменяется состояние приложения? Предположим, что использование функции «Работа» на копии состояния не представляется возможным.

Одним из решений было бы прослушивание изменения состояния в кеше задач (или там около). Если в кеше есть приложение CancellationToken, используемое основной задачей, тот, который выполняет функцию «Работа», может отменить его. Тогда это может привести к отмене всех подключенных TaskCompletionSources Task: s, и, таким образом, оба компонента пользовательского интерфейса получат отмененные задачи. Это своего рода отказ от снизу вверх.

Есть ли предпочтительный способ сделать это? Существует ли шаблон проектирования, который описывает его где-то?

Снизить снизу можно, но это немного странно. Задача UI создается с помощью CancellationToken, но отменяется из-за другого (внутреннего) CancellationToken. Кроме того, поскольку токены не совпадают, исключение OperationCancelledException нельзя просто игнорировать в пользовательском интерфейсе, что (в конечном итоге) приведет к тому, что исключение будет выбрано во внешнем финализаторе Task: s.

+0

Отличный вопрос, хотя моя первая реакция была "Т.Л., д-р" – dtb

+1

Так, для уточнения; у вас есть два пользователя результата задачи, и вы хотите, чтобы одна задача вычисляла значение для каждого состояния приложения; но вы хотите, чтобы каждый потребитель мог «отменить» ожидание результата задачи? – Tejs

+0

Да, но также и то, что изменение состояния должно отменять вычисление. – 4ZM

ответ

1

Это звучит, как вы хотите, жадный набор операций задач - у вас есть поставщик результата задачи, а затем построить задачу, поставленную для возврата первого завершения операции, например:

// Task Provider - basically, construct your first call as appropriate, and then 
// invoke this on state change 

public void OnStateChanged() 
{ 
    if(_cts != null) 
     _cts.Cancel(); 

    _cts = new CancellationTokenSource(); 
    _task = Task.Factory.StartNew(() => 
     { 
      // Do Computation, checking for cts.IsCancellationRequested, etc 
      return result; 
     }); 
} 

// Consumer 1 

var cts = new CancellationTokenSource(); 
var task = Task.Factory.StartNew(() => 
    { 
     var waitForResultTask = Task.Factory.StartNew(() => 
      { 
       // Internally, this is invoking the task and waiting for it's value 
       return MyApplicationState.GetComputedValue(); 
      }); 

     // Note this task cares about being cancelled, not the one above 
     var cancelWaitTask = Task.Factory.StartNew(() => 
     { 
       while(!cts.IsCancellationRequested) 
       Thread.Sleep(25); 

       return someDummyValue; 
     }); 

     Task.WaitAny(waitForResultTask, cancelWaitTask); 

     if(cancelWaitTask.IsComplete) 
      return "Blah"; // I cancelled waiting on the original task, even though it is still waiting for it's response 
     else 
      return waitForResultTask.Result; 
    }); 

Теперь я гавань» t полностью протестировал это, но он должен позволить вам «отменить» ожидание задачи, отменив токен (и, таким образом, принудительно завершить задачу «ждать» и нажать WaitAny) и позволить вам «отменить» задачу вычисления ,

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

+0

Я использовал класс TaskCompletionSource, чтобы делать то, что делает ваш потребитель. Но проблема такая же. Когда вызывается OnStateChange и отменяется «реальная» задача, функция MyApplicationState.GetComputedValue() выкинет OpCanceledException. Это будет повторно задано Task.WaitAny, а затем «задача» потребует особой осторожности, чтобы не взорвать его финализатор. – 4ZM

+1

Вот только если вы используете 'cts.ThrowIfCancellationIsRequested' - если вы просто проверяете условие в потоке, не генерируется исключение, если вы просто возвращаете фиктивное значение. Вы сами проверяете токен, а не как фреймворк. В качестве альтернативы вы можете просто обернуть «Task.WaitAny» внутренним try/catch и обработать исключение там, вернув возвращаемое значение «Отмененное» фиктивное (или что-то подходящее) – Tejs

+0

Вы правы. Использование специального возвращаемого значения для отмены (вместо использования исключений) является одним из способов реализации того, что я хочу. Я должен был бы ввести фиктивное значение (или новый тип), и, похоже, оно противоречит общему использованию обработки отмены с исключениями в инфраструктуре .Net Tasks. Я держу свои пальцы скрещенными для более аккуратного решения, но это сработает. Благодарю. – 4ZM

1

Вот моя попытка:

// the Task for the current application state 
Task<Result> _task; 
// a CancellationTokenSource for the current application state 
CancellationTokenSource _cts; 

// called when the application state changes 
void OnStateChange() 
{ 
    // cancel the Task for the old application state 
    if (_cts != null) 
    { 
     _cts.Cancel(); 
    } 

    // new CancellationTokenSource for the new application state 
    _cts = new CancellationTokenSource(); 
    // start the Task for the new application state 
    _task = Task.Factory.StartNew<Result>(() => { ... }, _cts.Token); 
} 

// called by UI component 
Task<Result> ComputeResultAsync(CancellationToken cancellationToken) 
{ 
    var task = _task; 
    if (cancellationToken.CanBeCanceled && !task.IsCompleted) 
    { 
     task = WrapTaskForCancellation(cancellationToken, task); 
    } 
    return task; 
} 

с

static Task<T> WrapTaskForCancellation<T>(
    CancellationToken cancellationToken, Task<T> task) 
{ 
    var tcs = new TaskCompletionSource<T>(); 
    if (cancellationToken.IsCancellationRequested) 
    { 
     tcs.TrySetCanceled(); 
    } 
    else 
    { 
     cancellationToken.Register(() => 
     { 
      tcs.TrySetCanceled(); 
     }); 
     task.ContinueWith(antecedent => 
     { 
      if (antecedent.IsFaulted) 
      { 
       tcs.TrySetException(antecedent.Exception.GetBaseException()); 
      } 
      else if (antecedent.IsCanceled) 
      { 
       tcs.TrySetCanceled(); 
      } 
      else 
      { 
       tcs.TrySetResult(antecedent.Result); 
      } 
     }, TaskContinuationOptions.ExecuteSynchronously); 
    } 
    return tcs.Task; 
} 
+0

Это что-то вроде того, что я сделал. Он работает, но хороший дизайн? С точки зрения пользовательских интерфейсов задача отменяется кем-то еще. С помощью этого решения пользовательский интерфейс также должен будет явно обрабатывать исключения из внутренних задач, если он создает дополнительные задачи-оболочки, чтобы исключить «незаметное исключение, возникшее из-за проблемы финализатора». – 4ZM