Un patrón para auto-cancelar y reiniciar la tarea

¿Existe un patrón establecido recomendado para tareas de cancelación automática y reinicio?

Por ejemplo, estoy trabajando en la API para el corrector ortográfico de fondo. La sesión de revisión ortográfica se envuelve como Task . Cada sesión nueva debe cancelar la anterior y esperar su finalización (para volver a utilizar correctamente los recursos como el proveedor de servicios de corrección ortográfica, etc.).

He llegado a algo como esto:

 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; } } 

La aplicación cliente (la IU) solo debería poder llamar a SpellcheckAsync tantas veces como lo desee, sin preocuparse por cancelar una sesión pendiente. El bucle doMore principal se ejecuta en el subproceso UI (ya que implica la interfaz de usuario, mientras que todas las llamadas al proveedor de servicios de revisión ortográfica están vinculadas a IO).

Me siento un poco incómodo por el hecho de que tuve que dividir la API en dos, SpellcheckAsync y SpellcheckAsyncHelper , pero no puedo pensar en una mejor manera de hacerlo, y aún no se ha probado.

Creo que el concepto general es bastante bueno, aunque recomiendo que no uses ContinueWith .

Solo lo escribiría usando la await normal, y mucha de la lógica “¿ya estoy corriendo?” No es necesaria:

 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; } } 

Esta es la versión más reciente del patrón cancelar y reiniciar que utilizo:

 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; } } } 

Uso de prueba (produciendo solo una salida única “Start / Done” para DoWorkAsync ):

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

Espero que esto sea útil: intenté crear una clase de ayuda que se pueda reutilizar:

 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); } 

Los ejemplos anteriores parecen tener problemas cuando se llama al método asíncrono varias veces rápidamente uno detrás del otro, por ejemplo, cuatro veces. Luego, todas las llamadas posteriores de este método cancelan la primera tarea y al final se generan tres tareas nuevas que se ejecutan al mismo tiempo. Así que se me ocurrió esto:

  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>(); }