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

时间:2016-02-10 12:01:33

标签: c# winforms async-await backgroundworker

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

3 个答案:

答案 0 :(得分:9)

我有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之前收集来自用户界面的所有信息并将其传递到Save方法,那么它可能会更好。

答案 1 :(得分:2)

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

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

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

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

  • 声明函数async
  • 而不是返回void返回Task而不是TResult返回Task&lt; TResult&gt;。
  • 此规则的唯一例外是事件处理程序。异步事件处理程序返回void。
  • 按顺序执行您的操作,并尽可能调用其他功能的异步版本。
  • 调用此异步函数不会立即执行它。相反,只要可用线程池中的线程准备就绪,就会执行它。
  • 这意味着在您的线程安排任务后,可以自由地执行其他操作
    • 当你的线程需要其他任务的结果等待tak。
    • await返回任务无效,返回等待任务&lt; TResult&gt;是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异步也很简单。因为我的所有评论似乎都是很多代码,但事实上它只是一个很小的功能。

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

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

要做的事情:如果fileSave遇到问题,我会对错误做出反应。考虑返回一个任务&lt; TResult&gt;而不是任务。

TResult fileSaveResult = fileSaveTask.Result;

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

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

答案 2 :(得分:1)

Stephen Cleary answer基本上涵盖了这个案例。但阻塞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元素),并且只使用传递的参数。