将后台工作程序更新为async-await

因此,这就是我目前使用后台工作程序将大量内容保存到文件中的方式,同时向用户显示进度条并防止在保存过程中对UI进行任何更改。 我想我已经掌握了基本function。 模态ProgressWindow显示进度条而不是其他内容。 如果必须的话,我将如何将其更改为async-await模式?

 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) { string path= e.Argument as string; // 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(); } 

我有一个博客系列 ,详细介绍了这一点。

简而言之, BackgroundWorkerTask.Run取代, ReportProgress (和朋友)被IProgress取代。

所以,简单的翻译看起来像这样:

 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(string path, IProgress progress) { await Task.Run(() => Save(path, progress)); ProgressWindow.Close(); } void Save(string 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 } 

改进注意事项:

  • 让后台线程进入UI( // get some stuff from UI )并不是一个好主意。 如果您可以调用Task.Run 之前从UI收集所有信息并将其传递给Save方法,那么它可能会更好。

我想你让另一个线程耗费时间的原因是因为你想让用户界面保持响应。 您的方法将满足此要求。

使用async-await的优点是代码看起来更加同步,而用户界面似乎是响应式的。 您不必使用Control.IsInvokeRequired等事件和函数,因为它是将完成工作的主线程。

async-await的缺点是,只要主线程确实在做某事(=没有等待任务完成),你的UI就没有响应。

话虽如此,使函数异步很容易:

  • 声明函数async
  • 而不是void返回Task而不是TResult返回Task
  • 此规则的唯一例外是事件处理程序。 异步事件处理程序返回void。
  • 按顺序执行您的操作,并尽可能调用其他函数的异步版本。
  • 调用此异步函数不会立即执行它。 相反,它被安排在可用线程池中的线程准备就绪时立即执行。
  • 这意味着在您的线程安排任务后,可以自由地执行其他操作
    • 当你的线程需要其他任务的结果等待tak。
    • 等待任务的返回无效,等待任务的返回是TResult。

所以要使你的函数异步:

异步SaveFile函数很简单:

 private async Task SaveFileAsync(string 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(...) } 

SaveButtonClick异步也很简单。 由于我的所有评论似乎很多代码,但实际上它是一个小function。

请注意,该函数是一个事件处理程序:return void而不是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 ); 

一旦线程池中的线程空闲,该任务就会被安排运行。 同时主线程有时间做其他事情,比如显示和更新进度窗口。

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

现在反复等一下,调整进度。 saveFileTask任务完成后立即停止。

我们不能让主线程等待任务完成,因为这会阻止UI响应,除了主线程应该重复更新进度条。

解决方案:不要使用Task.Wait函数,而是使用Task.When函数。 不同之处在于Task.When函数返回等待的任务,因此您可以等待任务完成,从而保持UI响应。

任务。当函数没有超时版本时。 为此,我们启动Task.Delay

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

一旦fileSaveTask完成,或者延迟任务完成,Task.WhenAny就会停止。

要做的事:如果fileSave遇到问题,我会对错误做出反应。 考虑返回Task 而不是Task。

 TResult fileSaveResult = fileSaveTask.Result; 

或抛出exception。 主窗口线程将其捕获为AggregateException。 InnerExceptions(复数)包含任何任务抛出的exception。

如果您需要能够停止保存过程,则需要将CacellationToken传递给每个函数并让SaveFile

Stephen Cleary的答案基本上涵盖了案例。 但阻塞ShowDialog调用会产生一个复杂因素,阻止正常的async/await流。

所以除了他的答案,我建议你使用以下一般助手function

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

然后删除ProgressWindow表单成员并使用以下内容

 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(string path, IProgress progress) { // as in Stephen's answer } 

请注意,我已经将实际的worker方法标记为static ,以防止访问内部的表单(以及任何UI元素)并仅使用传递的参数。