Шаблон для самоотдачи и перезапуска задачи

Существует ли рекомендованный шаблон для самостоятельной отмены и перезапуска задач?

Например, я работаю над API для проверки орфографии. Сеанс проверки орфографии завершается как Task . Каждый новый сеанс должен отменить предыдущий и дождаться его завершения (чтобы правильно использовать ресурсы, такие как поставщик услуг проверки орфографии и т. Д.).

Я придумал что-то вроде этого:

 class Spellchecker { Task pendingTask = null; // pending session CancellationTokenSource cts = null; // CTS for pending session // SpellcheckAsync is called by the client app public async Task SpellcheckAsync(CancellationToken token) { // SpellcheckAsync can be re-entered var previousCts = this.cts; var newCts = CancellationTokenSource.CreateLinkedTokenSource(token); this.cts = newCts; if (IsPendingSession()) { // cancel the previous session and wait for its termination if (!previousCts.IsCancellationRequested) previousCts.Cancel(); // this is not expected to throw // as the task is wrapped with ContinueWith await this.pendingTask; } newCts.Token.ThrowIfCancellationRequested(); var newTask = SpellcheckAsyncHelper(newCts.Token); this.pendingTask = newTask.ContinueWith((t) => { this.pendingTask = null; // we don't need to know the result here, just log the status Debug.Print(((object)t.Exception ?? (object)t.Status).ToString()); }, TaskContinuationOptions.ExecuteSynchronously); return await newTask; } // the actual task logic async Task SpellcheckAsyncHelper(CancellationToken token) { // do not start a new session if the the previous one still pending if (IsPendingSession()) throw new ApplicationException("Cancel the previous session first."); // do the work (pretty much IO-bound) try { bool doMore = true; while (doMore) { token.ThrowIfCancellationRequested(); await Task.Delay(500); // placeholder to call the provider } return doMore; } finally { // clean-up the resources } } public bool IsPendingSession() { return this.pendingTask != null && !this.pendingTask.IsCompleted && !this.pendingTask.IsCanceled && !this.pendingTask.IsFaulted; } } 

Клиентское приложение (UI) должно просто иметь возможность вызывать SpellcheckAsync столько раз, сколько SpellcheckAsync , не беспокоясь об отмене ожидающего сеанса. Основной цикл doMore выполняется в streamе пользовательского интерфейса (так как он включает в себя пользовательский интерфейс, в то время как все вызовы поставщика услуг проверки орфографии связаны с IO-привязкой).

Мне немного неудобно в том, что мне пришлось разделить API на два уровня: SpellcheckAsync и SpellcheckAsyncHelper , но я не могу придумать лучшего способа сделать это, и он еще не проверен.

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

Я бы просто написал его с помощью регулярного await , и много логики «я уже работаю» не нужно:

 Task pendingTask = null; // pending session CancellationTokenSource cts = null; // CTS for pending session // SpellcheckAsync is called by the client app on the UI thread public async Task SpellcheckAsync(CancellationToken token) { // SpellcheckAsync can be re-entered var previousCts = this.cts; var newCts = CancellationTokenSource.CreateLinkedTokenSource(token); this.cts = newCts; if (previousCts != null) { // cancel the previous session and wait for its termination previousCts.Cancel(); try { await this.pendingTask; } catch { } } newCts.Token.ThrowIfCancellationRequested(); this.pendingTask = SpellcheckAsyncHelper(newCts.Token); return await this.pendingTask; } // the actual task logic async Task SpellcheckAsyncHelper(CancellationToken token) { // do the work (pretty much IO-bound) using (...) { bool doMore = true; while (doMore) { token.ThrowIfCancellationRequested(); await Task.Delay(500); // placeholder to call the provider } return doMore; } } 

Вот самая последняя версия шаблона отмены и перезапуска, который я использую:

 class AsyncWorker { Task _pendingTask; CancellationTokenSource _pendingTaskCts; // the actual worker task async Task DoWorkAsync(CancellationToken token) { token.ThrowIfCancellationRequested(); Debug.WriteLine("Start."); await Task.Delay(100, token); Debug.WriteLine("Done."); } // start/restart public void Start(CancellationToken token) { var previousTask = _pendingTask; var previousTaskCts = _pendingTaskCts; var thisTaskCts = CancellationTokenSource.CreateLinkedTokenSource(token); _pendingTask = null; _pendingTaskCts = thisTaskCts; // cancel the previous task if (previousTask != null && !previousTask.IsCompleted) previousTaskCts.Cancel(); Func runAsync = async () => { // await the previous task (cancellation requested) if (previousTask != null) await previousTask.WaitObservingCancellationAsync(); // if there's a newer task started with Start, this one should be cancelled thisTaskCts.Token.ThrowIfCancellationRequested(); await DoWorkAsync(thisTaskCts.Token).WaitObservingCancellationAsync(); }; _pendingTask = Task.Factory.StartNew( runAsync, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.FromCurrentSynchronizationContext()).Unwrap(); } // stop public void Stop() { if (_pendingTask == null) return; if (_pendingTask.IsCanceled) return; if (_pendingTask.IsFaulted) _pendingTask.Wait(); // instantly throw an exception if (!_pendingTask.IsCompleted) { // still running, request cancellation if (!_pendingTaskCts.IsCancellationRequested) _pendingTaskCts.Cancel(); // wait for completion if (System.Threading.Thread.CurrentThread.GetApartmentState() == ApartmentState.MTA) { // MTA, blocking wait _pendingTask.WaitObservingCancellation(); } else { // TODO: STA, async to sync wait bridge with DoEvents, // similarly to Thread.Join } } } } // useful extensions public static class Extras { // check if exception is OperationCanceledException public static bool IsOperationCanceledException(this Exception ex) { if (ex is OperationCanceledException) return true; var aggEx = ex as AggregateException; return aggEx != null && aggEx.InnerException is OperationCanceledException; } // wait asynchrnously for the task to complete and observe exceptions public static async Task WaitObservingCancellationAsync(this Task task) { try { await task; } catch (Exception ex) { // rethrow if anything but OperationCanceledException if (!ex.IsOperationCanceledException()) throw; } } // wait for the task to complete and observe exceptions public static void WaitObservingCancellation(this Task task) { try { task.Wait(); } catch (Exception ex) { // rethrow if anything but OperationCanceledException if (!ex.IsOperationCanceledException()) throw; } } } 

Тестирование (создание только одного выхода «Пуск / Готово» для DoWorkAsync ):

 private void MainForm_Load(object sender, EventArgs e) { var worker = new AsyncWorker(); for (var i = 0; i < 10; i++) worker.Start(CancellationToken.None); } 

Надеюсь, это будет полезно – попытался создать class Helper, который можно повторно использовать:

 class SelfCancelRestartTask { private Task _task = null; public CancellationTokenSource TokenSource { get; set; } = null; public SelfCancelRestartTask() { } public async Task Run(Action operation) { if (this._task != null && !this._task.IsCanceled && !this._task.IsCompleted && !this._task.IsFaulted) { TokenSource?.Cancel(); await this._task; TokenSource = new CancellationTokenSource(); } else { TokenSource = new CancellationTokenSource(); } this._task = Task.Run(operation, TokenSource.Token); } 

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

  private List> _parameterExtractionTasks = new List>(); /// This method is asynchronous, ie it runs partly in the background. As this method might be called multiple times /// quickly after each other, a mechanism has been implemented that all tasks from previous method calls are first canceled before the task is started anew. public async void ParameterExtraction() { CancellationTokenSource newCancellationTokenSource = new CancellationTokenSource(); // Define the task which shall run in the background. Task newTask = new Task(() => { // do some work here } } }, newCancellationTokenSource.Token); _parameterExtractionTasks.Add(new Tuple(newTask, newCancellationTokenSource)); /* Convert the list to arrays as an exception is thrown if the number of entries in a list changes while * we are in a for loop. This can happen if this method is called again while we are waiting for a task. */ Task[] taskArray = _parameterExtractionTasks.ConvertAll(item => item.Item1).ToArray(); CancellationTokenSource[] tokenSourceArray = _parameterExtractionTasks.ConvertAll(item => item.Item2).ToArray(); for (int i = 0; i < taskArray.Length - 1; i++) { // -1: the last task, ie the most recent task, shall be run and not canceled. // Cancel all running tasks which were started by previous calls of this method if (taskArray[i].Status == TaskStatus.Running) { tokenSourceArray[i].Cancel(); await taskArray[i]; // wait till the canceling completed } } // Get the most recent task Task currentThreadToRun = taskArray[taskArray.Length - 1]; // Start this task if, but only if it has not been started before (ie if it is still in Created state). if (currentThreadToRun.Status == TaskStatus.Created) { currentThreadToRun.Start(); await currentThreadToRun; // wait till this task is completed. } // Now the task has been completed once. Thus we can recent the list of tasks to cancel or maybe run. _parameterExtractionTasks = new List>(); } 
  • Использовать Task.Run () в синхронном методе, чтобы избежать тупиковой остановки в асинхронном методе?
  • Почему это асинхронное действие зависает?
  • Запуск нескольких задач async и ожидание их завершения
  • ConfigureAwait подталкивает продолжение в stream пула
  • Разница между TPL и async / await (Обработка streamов)
  • Запуск задач в foreach Loop использует значение последнего элемента
  • Должны ли мы переключиться на использование асинхронного ввода-вывода по умолчанию?
  • Давайте будем гением компьютера.