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

本文关键字:异步 等待 更新 线程 后台 工作 | 更新日期: 2023-09-27 18:34:05

这就是我目前使用后台工作程序将大量内容保存到文件的方式,同时向用户显示进度条并防止在保存过程中对 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();
}

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

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

简而言之,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 之前从 UI 收集所有信息并将其传递到 Save 方法中,则可能会更好地工作。

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

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

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

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

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

因此,要使您的函数异步:

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

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 async 也很容易。由于我的所有评论,它似乎有很多代码,但实际上它是一个小函数。

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

现在反复等待一秒钟并调整进度。保存文件任务完成后立即停止。

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

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

任务。当功能没有超时版本时。为此,我们启动一个任务.延迟

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

一旦文件保存任务完成,或者延迟任务完成,任务就会停止。

要做的事情:如果文件保存遇到问题,则对错误做出反应。请考虑返回任务 而不是 Task。

TResult fileSaveResult = fileSaveTask.Result;

或引发异常。主窗口线程将其捕获为 AggregateException。InnerExceptions(复数(包含任何任务引发的异常。

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

Stephen Cleary的回答基本上涵盖了这种情况。但是,阻塞ShowDialog呼叫会带来一个复杂性,这会阻止正常的async/await流动。

所以除了他的回答之外,我还会建议你以下一般的助手函数

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 元素(,并且仅使用传递的参数。