Mise à jour du travailleur d’arrière-plan vers async-wait

C’est donc la façon dont j’utilise actuellement l’agent d’arrière-plan pour enregistrer beaucoup de choses dans un fichier tout en présentant à l’utilisateur une barre de progression et en empêchant toute modification de l’UI pendant la sauvegarde. Je pense avoir capturé les fonctionnalités essentielles. ProgressWindow modal affiche une barre de progression et pas grand-chose d’autre. Comment pourrais-je m’y prendre si je devais le changer?

 private ProgressForm ProgressWindow { get; set; } /// On clicking save button, save stuff to file void SaveButtonClick(object sender, EventArgs e) { if (SaveFileDialog.ShowDialog() == DialogResult.OK) { if (!BackgroundWorker.IsBusy) { BackgroundWorker.RunWorkerAsync(SaveFileDialog.FileName); ProgressWindow= new ProgressForm(); ProgressWindow.SetPercentageDone(0); ProgressWindow.ShowDialog(this); } } } /// Background worker task to save stuff to file void BackgroundWorkerDoWork(object sender, DoWorkEventArgs e) { ssortingng path= e.Argument as ssortingng; // open file for (int i=0; i < 100; i++) { // get some stuff from UI // save stuff to file BackgroundWorker.ReportProgress(i); } // close file } /// On background worker progress, report progress void BackgroundWorkerProgressChanged(object sender, ProgressChangedEventArgs e) { ProgressWindow.SetPercentageDone(e.ProgressPercentage); } /// On background worker finished, close progress form void BackgroundWorkerRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { ProgressWindow.Close(); } 

J’ai une série de blogs qui couvre cela en détail.

En bref, BackgroundWorker est remplacé par Task.Run et ReportProgress (et amis) est remplacé par IProgress .

Ainsi, une traduction simple ressemblerait à ceci:

 async void SaveButtonClick(object sender, EventArgs e) { if (SaveFileDialog.ShowDialog() == DialogResult.OK) { ProgressWindow = new ProgressForm(); ProgressWindow.SetPercentageDone(0); var progress = new Progress(ProgressWindow.SetPercentageDone); var task = SaveAndClose(SaveFileDialog.FileName, progress)); ProgressWindow.ShowDialog(this); await task; } } async Task SaveAndClose(ssortingng path, IProgress progress) { await Task.Run(() => Save(path, progress)); ProgressWindow.Close(); } void Save(ssortingng path, IProgress progress) { // open file for (int i=0; i < 100; i++) { // get some stuff from UI // save stuff to file if (progress != null) progress.Report(i); } // close file } 

Notes pour des améliorations:

  • Ce n'est généralement pas une bonne idée d'avoir des threads d'arrière-plan atteignant l'interface utilisateur ( // get some stuff from UI ). Cela fonctionnerait probablement mieux si vous pouviez collecter toutes les informations de l'interface utilisateur avant d' appeler Task.Run et simplement les transmettre à la méthode Save .

Je suppose que la raison pour laquelle vous laissez un autre thread faire le travail qui prend du temps est que vous voulez que l’interface utilisateur rest sensible. Votre méthode répondra à cette exigence.

L’avantage d’utiliser async-wait est que le code aura l’air plus synchrone, tandis que l’interface utilisateur semble être réactive. Vous n’avez pas à travailler avec des événements et des fonctions comme Control.IsInvokeRequired, car c’est le thread principal qui effectuera le travail.

L’inconvénient de async-wait est que tant que le thread principal fait vraiment quelque chose (= n’attend pas qu’une tâche se termine), votre interface utilisateur n’est pas réactive.

Ceci dit, rendre une fonction asynchrone est facile:

  • déclarer la fonction async
  • À la place de void return Task et à la place de TResult return Task .
  • La seule exception à cette règle concerne les gestionnaires d’événements. Un gestionnaire d’événement asynchrone renvoie null.
  • Faites votre travail de manière séquentielle et, si possible, appelez les versions asynchrones des autres fonctions.
  • L’appel de cette fonction asynchrone ne l’exécute pas immédiatement. Au lieu de cela, il est programmé pour être exécuté dès qu’un thread du pool de thread disponibles est prêt à le faire.
  • Cela signifie qu’après que votre thread a planifié la tâche, il est libre de faire d’autres choses.
    • Lorsque votre thread a besoin du résultat de l’autre tâche, attendez le début.
    • Le retour de wait, la tâche est vide, le retour de wait, la tâche est TResult.

Donc, pour rendre votre fonction asynchrone:

La fonction SaveFile asynchrone est simple:

 private async Task SaveFileAsync(ssortingng fileName) { // this async function does not know // and does not have to know that a progress bar is used // to show its process. All it has to do is save ... // prepare the data to save, this may be time consuming // but this is not the main thread, so UI still responding // after a while do the saving and use other async functions using (TextWriter writer = ...) { var writeTask = writer.WriteAsync (...) // this thread is free to do other things, // for instance prepare the next item to write // after a while wait until the writer finished writing: await writeTask; // of course if you had nothing to do while writing // you could write: await writer.WriteAsync(...) } 

L’async SaveButtonClick est également facile. À cause de tout mon commentaire, cela semble beaucoup de code, mais en fait c’est une petite fonction.

Notez que la fonction est un gestionnaire d’événements: return void au lieu de Task

 private async void SaveButtonClick(object sender, EventArgs e) { if (SaveFileDialog.ShowDialog() == DialogResult.OK) { // start a task to save the file, but don't wait for it to finish // because we need to update the progress bar var saveFileTask = Task.Run () => SaveFileAsync ( SaveFileDialog.FileName ); 

La tâche est planifiée pour s’exécuter dès qu’un thread du pool de threads est libre. Pendant ce temps, le thread principal a le temps de faire d’autres choses, comme afficher et mettre à jour la fenêtre de progression.

  this.ProgressWindow.Visible = true; this.ProgressWindow.Value = ... 

Maintenant, attendez à plusieurs resockets et ajustez les progrès. Arrêtez dès que la tâche saveFileTask est terminée.

Nous ne pouvons pas simplement laisser le thread principal attendre la fin de la tâche car cela empêcherait l’interface utilisateur de répondre, sinon le thread principal devrait mettre à jour la barre de progression à plusieurs resockets.

Solution: N’utilisez pas les fonctions Task.Wait, mais les fonctions Task.When. La différence réside dans le fait que Tâche. Lorsque les fonctions renvoient des tâches en attente, vous pouvez donc attendre que la tâche se termine pour que l’interface utilisateur rest sensible.

La tâche. Lorsque les fonctions n’ont pas de version à expiration. Pour cela, nous commençons un Task.Delay

  while (!fileSaveTask.IsCompleted) { await Task.WhenAny( new Task[] { fileSaveTask, Task.Delay(TimeSpan.FromSeconds(1)), }; if (!fileSaveTask.IsCompleted this.UpdateProgressWindow(...); } 

Task.WhenAny s’arrête dès que la tâche fileSaveTask est terminée ou si la tâche de délai est terminée.

Ce qu’il faut faire: réagir aux erreurs si fileSave rencontre des problèmes. Envisagez de renvoyer une tâche au lieu d’une tâche.

 TResult fileSaveResult = fileSaveTask.Result; 

ou jeter une exception. Le thread de la fenêtre principale détecte cela comme une exception AggregateException. Les InnerExceptions (pluriel) contiennent les exceptions levées par l’une des tâches.

Si vous devez pouvoir arrêter le processus de sauvegarde, vous devez transmettre un CacellationToken à chaque fonction et laisser le fichier SaveFile à la place.

La réponse de Stephen Cleary couvre essentiellement le cas. Mais il existe une complication imposée par l’appel bloquant de ShowDialog qui empêche le stream async/await normal.

Donc, en plus de sa réponse, je vous suggérerais la fonction d’assistance générale suivante

 public static class AsyncUtils { public static Task ShowDialogAsync(this Form form, IWin32Window owner = null) { var tcs = new TaskCompletionSource(); EventHandler onShown = null; onShown = (sender, e) => { form.Shown -= onShown; tcs.TrySetResult(null); }; form.Shown += onShown; SynchronizationContext.Current.Post(_ => form.ShowDialog(owner), null); return tcs.Task; } } 

Ensuite, supprimez le membre du formulaire ProgressWindow et utilisez le code suivant.

 async void SaveButtonClick(object sender, EventArgs e) { if (SaveFileDialog.ShowDialog() == DialogResult.OK) { using (var progressWindow = new ProgressForm()) { progressWindow.SetPercentageDone(0); await progressWindow.ShowDialogAsync(this); var path = SaveFileDialog.FileName; var progress = new Progress(progressWindow.SetPercentageDone); await Task.Run(() => Save(path, progress)); } } } static void Save(ssortingng path, IProgress progress) { // as in Stephen's answer } 

Notez que j’ai marqué la méthode de travail réelle static afin d’empêcher l’access au formulaire (et à tout élément d’interface utilisateur) et de ne travailler qu’avec les arguments transmis.