将后台工作者更新为异步等待

Updating background worker to async-await

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

private ProgressForm ProgressWindow { get; set; }

/// <summary>On clicking save button, save stuff to file</summary>
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);
    }
  }
}

/// <summary>Background worker task to save stuff to file</summary>
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
}

/// <summary>On background worker progress, report progress</summary>
void BackgroundWorkerProgressChanged(object sender, ProgressChangedEventArgs e)
{
  ProgressWindow.SetPercentageDone(e.ProgressPercentage);
}

/// <summary>On background worker finished, close progress form</summary>
void BackgroundWorkerRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
  ProgressWindow.Close();
}

我有一个 blog series 详细介绍了这一点。

简而言之,BackgroundWorkerTask.Run代替,ReportProgress(和朋友)被IProgress<T>代替。

所以,一个简单的翻译应该是这样的:

async void SaveButtonClick(object sender, EventArgs e)
{
  if (SaveFileDialog.ShowDialog() == DialogResult.OK)
  {
    ProgressWindow = new ProgressForm();
    ProgressWindow.SetPercentageDone(0);
    var progress = new Progress<int>(ProgressWindow.SetPercentageDone);
    var task = SaveAndClose(SaveFileDialog.FileName, progress));
    ProgressWindow.ShowDialog(this);
    await task;
  }
}

async Task SaveAndClose(string path, IProgress<int> progress)
{
  await Task.Run(() => Save(path, progress));
  ProgressWindow.Close();
}

void Save(string path, IProgress<int> 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 就没有响应。

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

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

所以要让你的函数异步:

异步保存文件功能很简单:

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 异步也很简单。由于我的所有评论看起来代码很多,但实际上它是一个小功能。

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

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

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

现在反复稍等片刻调整进度。 saveFileTask 任务完成后立即停止。

我们不能让主线程等待任务完成,因为那样会停止 UI 响应,此外主线程应该重复更新进度条。

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

Task.When 函数没有超时版本。为此,我们开始 Task.Delay

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

Task.WhenAny 在 fileSaveTask 完成或延迟任务完成后立即停止。

要做的事情:如果 fileSave 遇到问题,对错误做出反应。考虑 return 使用 Task < TResult > 而不是 Task。

TResult fileSaveResult = fileSaveTask.Result;

或者抛出异常。主 window 线程将其捕获为 AggregateException。 InnerExceptions(复数)包含任何任务抛出的异常。

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

基本上涵盖了这个案例。但是,阻止正常的 async/await 流程的阻塞 ShowDialog 调用带来了一种复杂情况。

因此,除了他的回答之外,我还建议您使用以下通用辅助函数

public static class AsyncUtils
{
    public static Task ShowDialogAsync(this Form form, IWin32Window owner = null)
    {
        var tcs = new TaskCompletionSource<object>();
        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<int>(progressWindow.SetPercentageDone);
            await Task.Run(() => Save(path, progress));
        }
    }
}

static void Save(string path, IProgress<int> progress)
{
    // as in Stephen's answer
}

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