C#异步/等待:在Task<>对象上使用进度事件

46

我对C# 5新的async/await关键字完全不熟悉,我想知道如何最佳实现进度事件。

我希望Progress事件在Task<>本身上。我知道我可以将事件放在包含异步方法的类中,并在事件处理程序中传递某种状态对象,但对我来说,这似乎更像是一种变通而非解决方案。我可能还希望不同的任务在不同的对象中触发事件处理程序,这种方式听起来很混乱。

是否有一种类似于以下方式的方法:

var task = scanner.PerformScanAsync();
task.ProgressUpdate += scanner_ProgressUpdate;
return await task;
4个回答

67

推荐的方法在基于任务的异步模式文档中有描述,该方法为每个异步方法提供了自己的IProgress<T>

public async Task PerformScanAsync(IProgress<MyScanProgress> progress)
{
  ...
  if (progress != null)
    progress.Report(new MyScanProgress(...));
}

用法:

var progress = new Progress<MyScanProgress>();
progress.ProgressChanged += ...
PerformScanAsync(progress);

注意:

  1. 按照惯例,如果调用者不需要进度报告,则progress参数可以为null,因此请确保在您的async方法中检查此项。
  2. 进度报告本身是异步的,所以每次调用时,您应该创建一个新的参数实例(更好的做法是只使用不可变类型作为事件参数)。您不应该改变并重复使用相同的参数对象来调用多个Progress
  3. Progress<T>类型将在构造时捕获当前上下文(例如,UI上下文),并在该上下文中引发其ProgressChanged事件。因此,在调用Report之前无需担心是否需要返回到UI线程。

2
只是为了我理解,也许能帮助未来的其他人...你提出第二点的原因是,如果在Report方法完成之前对传递给Progress.Report()的可变对象进行更改,则会导致未报告原始值? - Dave Williams
4
几乎是这样。我的意思是,当Report方法完成(并返回)时,实际的报告尚未发生。如果您的T是可变的,并且在传递给Report之后更改了它,则会出现竞争条件。实际报告可能会看到旧值、新值或混合值(即某些字段的旧值和某些字段的新值)。 - Stephen Cleary
是的,现在我明白得多了,谢谢。我读了你的一些博客等,你提到了竞态条件,但直到我读到这个问题时才立刻明显。谢谢。 - Dave Williams
那么,ProgressChanged委托是否没有保证按顺序获取Report()中的内容呢?据我理解,即使传递的对象是不可变的,如果我使用最新收到的T更新接口,则有可能会将其更新为显示更旧信息的状态,因为我在较新的进度对象之后收到了较旧的进度对象。 - deed02392
我在发布了上面的内容后意识到你的意思是我们不应该改变传递给Report()的进度对象,因为Report()会立即返回,所以你不知道接口另一端的处理程序实际上会收到什么关于进度对象值的信息。顺便问一下,你所说的技术上没有排序要求,但实现确实保留它是什么意思?不保留排序的实现难道不很过时吗? - deed02392
显示剩余2条评论

49
简单来说,Task不支持进度。然而,已经有一种常规的方法来实现这一点,使用IProgress<T>接口。基本上,基于任务的异步模式建议在异步方法(有意义的情况下)中重载,以允许客户端传递一个IProgress<T>实现。然后,您的异步方法将通过该接口报告进度。
Windows Runtime(WinRT)API内置了进度指示器,即IAsyncOperationWithProgress<TResult, TProgress>IAsyncActionWithProgress<TProgress>类型...因此,如果您实际上是为WinRT编写代码,那么这些类型值得研究 - 但是请阅读下面的注释。

3
对于WinRT来说,编写一个遵循C#惯例的带有IProgress<T>的普通异步方法,然后使用AsyncInfo.Run将其包装成IAsyncOperationWithProgress /IAsyncActionWithProgress,比直接实现这些接口要容易得多。 - Stephen Cleary
@StephenCleary:这取决于您是考虑“调用方”还是“实现”。如果您正在尝试公开供他人调用的功能,则我认为WinRT“本机”类型更合适。显然,两者之间有各种转换器。 - Jon Skeet
1
在C#中遵循C#规范并在边界处使用包装器(AsTask/AsyncInfo)会更容易。因此,用C#实现的WinRT组件将返回WinRT接口类型,但将使用常规的async包装在AsyncInfo中进行实现。 - Stephen Cleary
@StephenCleary:好的,我相信你的话 :) - Jon Skeet

11

我不得不从几篇帖子中拼凑出这个答案,因为我试图弄清楚如何让这个代码在处理更加复杂(例如事件通知的变化)的情况下运行。

假设您有一个同步项处理器,它将宣布它即将开始处理的项目编号。对于我的示例,我只是要操作“处理”按钮的内容,但您也可以轻松更新进度条等。

private async void BtnProcess_Click(object sender, RoutedEventArgs e)
{       
    BtnProcess.IsEnabled = false; //prevent successive clicks
    var p = new Progress<int>();
    p.ProgressChanged += (senderOfProgressChanged, nextItem) => 
                    { BtnProcess.Content = "Processing page " + nextItem; };

    var result = await Task.Run(() =>
    {
        var processor = new SynchronousProcessor();

        processor.ItemProcessed += (senderOfItemProcessed , e1) => 
                                ((IProgress<int>) p).Report(e1.NextItem);

        var done = processor.WorkItWorkItRealGood();

        return done ;
    });

    BtnProcess.IsEnabled = true;
    BtnProcess.Content = "Process";
}

关键部分是在ItemProcessed订阅中闭合Progress<>变量。这使得一切都可以“Just works ™”。


不确定“SynchronousProcessor”是从哪里来的。 - Feng Jiang
1
@FengJiang,那是你的代码需要同步处理,无论它是什么。这是一个示例,说明如何使用你的处理器的ItemProcessed事件绑定到ProgressChanged事件,以允许异步UI更新。 - Chris Marisic

0

在使用 Task.Run 的 lambda 时,我在其中使用了 Invoke Action 来更新 ProgressBar 控件。这可能不是最好的方法,但在紧急情况下,可以避免重构任何内容。

   Invoke(new Action(() =>

               {
                   LogProgress();
               }));

这将执行...

        private void LogProgress()
        {       
          progressBar1.Value = Convert.ToInt32((100 * (1.0 * LinesRead / TotalLinesToRead)));
        }

这将在主 GUI 线程上更新您的 progressBar,因此如果循环太大,它将使表单无响应。 - Ahmed Suror

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