异步方法的进度报告延迟

8

我有一个WinForms应用程序,其中包含按钮和RichTextBox控件。用户单击按钮后,将执行IO操作。为了防止UI线程被阻塞,我已经实现了异步/等待模式。我还希望将此操作的进度报告到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<String>Progress?它们的来源在哪里?它们似乎对你的问题非常重要。 - Enigmativity
2
它们是async-await框架的一部分,请参见:https://learn.microsoft.com/en-us/dotnet/api/system.iprogress-1?view=netframework-4.7.1 - Markkknk
@Enigmativity 这些是标准类(存储在 "mscorlib" dll 中),始于 .NET 4.5。 - Evk
2
@Markkknk - 阅读文档后,谢谢你,问题似乎在于 progress?.Report(p => this.Log(p) 的执行推到了 SychronizationContext 上。这意味着它必须等待 UI 消息循环空闲才能执行该代码。我建议您尝试删除 progress 调用并直接写入日志。我猜问题就会消失。 - Enigmativity
2个回答

8

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
@Markkknk,至少了解发生了什么,您可以做出明智的决策,如何在您的实际情况下解决这个问题。 - Evk

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


await Task.Yield(); 

作为 HeavyIO 方法的最后一句话,在返回结果之前。
原因是,正如先前所说 - 您需要允许进度报告由 UI 线程处理,而 Task.Yield() 正是这样做的。

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