使用进度/取消功能的异步/等待长时间运行的API方法

7

编辑 我认为强制 await 异步调用 worker 的正确方式是使用 Task.Run,像这样:


await Task.Run(() => builder.Build(dlg.FileName, cts.Token, new Progress(ReportProgress)));

http://blogs.msdn.com/b/pfxteam/archive/2012/04/12/10293335.aspx中得到了一些启示。


这应该很容易,但我对async/await很陌生,请耐心等待。我正在构建一个公开API的类库,其中包含一些长时间运行的操作。过去,我使用BackgroundWorker来处理进度报告和取消,就像这个简化的代码片段:


public void DoSomething(object sender, DoWorkEventArgs e)
{
            BackgroundWorker bw = (BackgroundWorker)sender;
            // e.Argument is any object as passed by consumer via RunWorkerAsync...

            do
            {
                // ... do something ...

                // abort if requested
                if (bw.CancellationPending)
                {
                    e.Cancel = true;
                    break;
                } //eif

                // notify progress
                bw.ReportProgress(nPercent);
            }
}

客户端代码如下:


BackgroundWorker worker = new BackgroundWorker
{ WorkerReportsProgress = true,
    WorkerSupportsCancellation = true
};
worker.DoWork += new DoWorkEventHandler(_myWorkerClass.DoSomething);
worker.ProgressChanged += WorkerProgressChanged;
worker.RunWorkerCompleted += WorkerCompleted;
worker.RunWorkerAsync(someparam);

现在我想利用新的异步模式。首先,这是我如何在我的API中编写一个简单的长时间运行的方法;在这里,我只是逐行读取文件,以模拟一个需要转换文件格式并进行一些处理的真实流程:


public async Task DoSomething(string sInputFileName, CancellationToken? cancel, IProgress progress)
{
    using (StreamReader reader = new StreamReader(sInputFileName))
    {
        int nLine = 0;
        int nTotalLines = CountLines(sInputFileName);

        while ((sLine = reader.ReadLine()) != null)
        {
            nLine++;
            // do something here...
      if ((cancel.HasValue) && (cancel.Value.IsCancellationRequested)) break;
      if (progress != null) progress.Report(nLine * 100 / nTotalLines);
        }
        return nLine;
    }
}

为了这个示例,假设这是DummyWorker类的一个方法。现在,这是我的客户端代码(一个WPF测试应用程序):


private void ReportProgress(int n)
{
    Dispatcher.BeginInvoke((Action)(() => { _progress.Value = n; }));
}

private async void OnDoSomethingClick(object sender, RoutedEventArgs e)
{
    OpenFileDialog dlg = new OpenFileDialog { Filter = "Text Files (*.txt)|*.txt" };
    if (dlg.ShowDialog() == false) return;

        // show the job progress UI...

    CancellationTokenSource cts = new CancellationTokenSource();
    DummyWorker worker = new DummyWorker();
    await builder.Build(dlg.FileName, cts.Token, new Progress(ReportProgress));

    // hide the progress UI...
}

IProgress接口的实现代码来自于http://blog.stephencleary.com/2010/06/reporting-progress-from-tasks.html,你可以参考该网址。无论如何,在这个应用测试中,用户界面被有效地阻塞,我看不到任何进展。因此,在这种情况下,与消费者代码相关的完整图像是什么?


是的,Task.Run() 是实现这个的方法。或者如果操作确实很长,可能需要使用 Task.Factory.StartNew() 并设置 LongRunning 选项。 - svick
1个回答

9

如上述博客文章顶部所注明的那样,该文章中的信息已经过时。您应该使用.NET 4.5提供的新IProgress<T> API。

如果您正在使用阻塞I/O,则应将核心方法设为阻塞:

public void Build(string sInputFileName, CancellationToken cancel, IProgress<int> progress)
{
  using (StreamReader reader = new StreamReader(sInputFileName))
  {
    int nLine = 0;
    int nTotalLines = CountLines(sInputFileName);

    while ((sLine = reader.ReadLine()) != null)
    {
      nLine++;
      // do something here...
      cancel.ThrowIfCancellationRequested();
      if (progress != null) progress.Report(nLine * 100 / nTotalLines);
    }

    return nLine;
  }
}

当您调用它时,将其包装在 Task.Run 中:

private async void OnDoSomethingClick(object sender, RoutedEventArgs e)
{
  OpenFileDialog dlg = new OpenFileDialog { Filter = "Text Files (*.txt)|*.txt" };
  if (dlg.ShowDialog() == false) return;

  // show the job progress UI...

  CancellationTokenSource cts = new CancellationTokenSource();
  DummyWorker worker = new DummyWorker();
  var progress = new Progress<int>((_, value) => { _progress.Value = value; });
  await Task.Run(() => builder.Build(dlg.FileName, cts.Token, progress);

  // hide the progress UI...
}

另外一种方法是,可以重写 Build 方法来使用异步API,然后直接在事件处理程序中调用它,而不需要将其包装在 Task.Run 中。


谢谢,我通过移除异步并将返回类型更改为void来使我的方法阻塞。 - Naftis
注意:您不允许在Build()方法中访问任何表单UI元素。 - Lefteris E
@LefterisE:这就是进度报告器的作用。 - Stephen Cleary
@StephenCleary,您能详细说明一下“异步API,然后直接从事件处理程序中调用”吗?我一直认为(推荐的)方法是通过在Task.Run()中包装同步任务来完成。 - Syaiful Nizam Yahya
2
@publicENEMY:完全不是这样的;Task.Run应该仅用于从UI线程调用的同步代码,而您无法使其异步化。最好从“叶子”开始处理诸如HTTP请求和DB访问之类的事情,使它们使用*Async方法,并允许异步代码向UI增长。 - Stephen Cleary

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接