2013-09-25 10 views
12

Esiste uno schema raccomandato per le attività di annullamento automatico e di riavvio?Un motivo per l'attività di annullamento automatico e riavvio

E.g., Sto lavorando all'API per il controllo ortografico di sfondo. La sessione di controllo ortografico è impacchettata come Task. Ogni nuova sessione dovrebbe annullare quella precedente e attendere la sua conclusione (per riutilizzare correttamente le risorse come provider di servizi di controllo ortografico, ecc.).

mi è venuta in mente qualcosa di simile:

class Spellchecker 
{ 
    Task pendingTask = null; // pending session 
    CancellationTokenSource cts = null; // CTS for pending session 

    // SpellcheckAsync is called by the client app 
    public async Task<bool> 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<bool> 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; 
    } 
} 

L'applicazione client (l'interfaccia utente) dovrebbe essere solo in grado di chiamare SpellcheckAsync tutte le volte che lo si desidera, senza preoccuparsi di cancellazione di una sessione in corso. Il ciclo principale doMore viene eseguito sul thread dell'interfaccia utente (poiché coinvolge l'interfaccia utente, mentre tutte le chiamate del provider di servizi di controllo ortografico sono vincolate all'IO).

Mi sento un po 'a disagio per il fatto che ho dovuto dividere l'API in due parti, SpellcheckAsync e SpellcheckAsyncHelper, ma non riesco a pensare a un modo migliore di farlo e deve ancora essere testato.

risposta

15

Penso che il concetto generale sia piuttosto buono, anche se consiglio di non utilizzare ContinueWith.

avevo appena scrivere utilizzando normale await, e un sacco di logica "Sono già in corso" non è necessario:

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<bool> 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<bool> 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; 
    } 
} 
+0

@Stephen Cleary, ho un immenso rispetto per il tuo lavoro su tutte le cose asincrone, quindi per favore non prendertela nel modo sbagliato: sono semplicemente curioso. Sono in qualche modo sorpreso che tu non abbia riscritto la parte 'await this.pendingTask' usando un' SemaphoreSlim' o il tuo 'AsyncLock' o simile. Credi generalmente che migliorare la sicurezza del thread nelle porzioni "sincrone" dei metodi asincroni sia un'ottimizzazione prematura? –

+0

@KirillShlenskiy: Non c'è niente di sbagliato nell'usare 'SemaphoreSlim' o simili per una restrizione una tantum. –

5

Ecco la versione più recente del modello cancel-e-restart che io uso:

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<Task> 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 di prova (che produce solo un singolo "Avvio/Fine" uscita per DoWorkAsync):

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

Una versione più recente e funzionale di questo modello è qui: http://stackoverflow.com/a/21427264/1768303 – Noseratio

0

speranza questo sarà utile - cercato di creare Classe helper che possono essere riutilizzati:

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

Gli esempi sopra sembrano avere problemi quando il metodo asincrono viene chiamato più volte rapidamente dopo l'altro, ad esempio quattro volte. Quindi tutte le chiamate successive di questo metodo annullano la prima attività e alla fine vengono generate tre nuove attività che vengono eseguite contemporaneamente. Quindi mi è venuto in mente questo:

private List<Tuple<Task, CancellationTokenSource>> _parameterExtractionTasks = new List<Tuple<Task, CancellationTokenSource>>(); 

    /// <remarks>This method is asynchronous, i.e. it runs partly in the background. As this method might be called multiple times 
    /// quickly after each other, a mechanism has been implemented that <b>all</b> tasks from previous method calls are first canceled before the task is started anew.</remarks> 
    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<Task, CancellationTokenSource>(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, i.e. 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 (i.e. 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<Tuple<Task, CancellationTokenSource>>(); 
    } 
Problemi correlati