【问题标题】:Delayed progress reporting from async method来自异步方法的延迟进度报告
【发布时间】:2018-05-08 09:57:24
【问题描述】:

我有一个包含 Button 和 RichTextBox 控件的 WinForms 应用程序。用户点击 Button 后,执行 IO 需求操作。为了防止 UI 线程阻塞,我实现了 async/await 模式。我还想将此操作的进度报告到 RichTextBox 中。简化的逻辑如下所示:

private async void LoadData_Click(Object sender, EventArgs e)
{
    this.LoadDataBtn.Enabled = false;

    IProgress<String> progressHandler = new Progress<String>(p => this.Log(p));

    this.Log("Initiating work...");

    List<Int32> result = await this.HeavyIO(new List<Int32> { 1, 2, 3 }, progressHandler);

    this.Log("Done!");

    this.LoadDataBtn.Enabled = true;
}

private async Task<List<Int32>> HeavyIO(List<Int32> ids, IProgress<String> progress)
{
    List<Int32> result = new List<Int32>();

    foreach (Int32 id in ids)
    {
        progress?.Report("Downloading data for " + id);

        await Task.Delay(500); // Assume that data is downloaded from the web here.

        progress?.Report("Data loaded successfully for " + id);

        Int32 x = id + 1; // Assume some lightweight processing based on downloaded data.

        progress?.Report("Processing succeeded for " + id);

        result.Add(x);
    }

    return result;
}

private void Log(String message)
{
    message += Environment.NewLine;
    this.RichTextBox.AppendText(message);
    Console.Write(message);
}

操作成功完成后,RichTextBox 包含以下文本:

Initiating work...
Downloading data for 1
Data loaded successfully for 1
Processing succeeded for 1
Downloading data for 2
Data loaded successfully for 2
Processing succeeded for 2
Downloading data for 3
Done!
Data loaded successfully for 3
Processing succeeded for 3

如您所见,第三个工作项的进度报告在 Done! 之后。

我的问题是,导致进度报告延迟的原因是什么?我怎样才能实现LoadData_Click 的流程只有在报告所有进度后才会继续?

【问题讨论】:

  • 什么是IProgress&lt;String&gt; & Progress?它们的来源在哪里?它们对您的问题似乎很重要。
  • 它们是 async-await 框架的一部分,请参阅:docs.microsoft.com/en-us/dotnet/api/…
  • @Enigmativity 是标准类(位于“mscorlib”dll 中),从 .NET 4.5 开始
  • @Markkknk - 所以,在阅读了文档之后,谢谢,问题似乎是progress?.Report( 正在将p =&gt; this.Log(p) 的执行推到SychronizationContext。这意味着它必须等到 UI 消息循环空闲后才能执行该代码。我建议您尝试删除progress 调用并直接写入日志。我怀疑问题会消失。

标签: c# winforms asynchronous async-await


【解决方案1】:

Progress 类将在创建时捕获当前同步上下文,然后将回调发布到该上下文(这在该类的文档中有所说明,或者您可以查看源代码)。在您的情况下,这意味着WindowsFormsSynhronizationContext 被捕获,并且发布到它与Control.BeginInvoke() 大致相同。

await 还捕获当前上下文(除非您使用ConfigureAwait(false))并将继续向其发布方法。对于除最后一次之外的迭代,UI 线程在await Task.Delay(500); 上发布,因此可以处理您的报告回调。但是在你的 foreach 循环的最后一次迭代中,会发生以下情况:

// context is captured
await Task.Delay(500); // Assume that data is downloaded from the web here.
// we are now back on UI thread
progress?.Report("Data loaded successfully for " + id);
// this is the same as BeginInvoke - this puts your callback in UI thread
// message queue
Int32 x = id + 1; // Assume some lightweight processing based on downloaded data.
// this also puts callback in UI thread queue and returns
progress?.Report("Processing succeeded for " + id);
result.Add(x);

因此,在上一次迭代中,您的回调被放入 UI 线程消息队列中,但它们现在无法执行,因为您同时在 UI 线程中执行代码。当代码到达this.Log("done") - 它被写入您的日志控件(这里不使用BeginInvoke)。然后在您的 LoadData_Click 方法结束后 - 只有此时 UI 线程才从执行您的代码中释放,并且消息队列可能会被处理,因此您在那里等待的 2 个回调已解决。

鉴于所有这些信息 - 就像 Enigmativity 在评论中所说的那样直接Log - 这里不需要使用Progress 类。

【讨论】:

  • 感谢您的解释。问题是,在我的非简化解决方案中,IO 逻辑(HeavyIO 方法)驻留在单独的类似工作者的类中。目前,我通过在HeavyIO 方法中添加Action 参数来引入对Log 方法的引用,从而解决了我的问题。我将您的答案标记为已接受。谢谢!
  • @Markkknk 至少知道发生了什么你可以做出明智的决定如何在你的实际案例中解决这个问题。
【解决方案2】:

你的代码是完全正确的,你唯一需要的是添加

await Task.Yield(); 

作为 HeavyIO 方法的最后一句,就在返回结果之前。

原因是,如前所述——您需要允许 UI 线程处理进度报告,而 Task.Yield() 正是这样做的。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-06-13
    • 1970-01-01
    • 2021-02-14
    • 2011-07-29
    • 2018-08-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多