首页 > 解决方案 > 为什么在涉及异常处理时异步等待非常慢并且阻塞?

问题描述

private void buttonLoad_Click(object sender, EventArgs e)
{
    DataTable dt = new DataTable(); //create datatable with 6000 records
    dt.Columns.Add("Name");
    dt.Columns.Add("Value");

    for (int i = 0; i < 2000; i++)
    {
        dt.Rows.Add("Tim", "955");
        dt.Rows.Add("Rob", "511");
        dt.Rows.Add("Steve", "201");
    }

    dataGridView1.DataSource = dt;
}
private async void btnSend_Click(object sender, EventArgs e)
{
    progressBar1.Minimum = 1;
    progressBar1.Maximum = dataGridView1.Rows.Count;
    progressBar1.Value = 1;
    progressBar1.Step = 1;

    List<Task> lstTasks = new List<Task>();

    DataTable dt = (DataTable)dataGridView1.DataSource;

    System.Diagnostics.Stopwatch s = new System.Diagnostics.Stopwatch();
    s.Start();
    foreach (DataRow dr in dt.Rows)
    {
        lstTasks.Add(CallAPIAsync(dr));
    }
    await Task.WhenAll(lstTasks);
    
    MessageBox.Show(s.ElapsedMilliseconds.ToString());
}
private async Task CallAPIAsync(DataRow dr)
{
    try
    {
        await Task.Delay(2000); //simulate post api request that will pass dr[name] and dr[value]

        if (new Random().Next(0,100)>95) //simulate error in the above request
        {
            throw new Exception("Test!");
        }
    }
    catch (Exception e)
    {
        Thread.Sleep(1);//similate sync processing that takes 1ms                
    }

    progressBar1.PerformStep();
}

buttonLoad_Click我将示例数据加载到数据表中。

btnSend_Click我模拟一个异步任务。


在异步任务中,如果您更改

if (new Random().Next(0,100)>95)

if (new Random().Next(0,100)>5)

为了模拟更多的异常,即使catch块只需要1ms,代码也会运行得很慢。

为什么在涉及异常处理时异步等待非常慢并且阻塞?

标签: c#asynchronousasync-await

解决方案


虽然评论中已经有一些很好的提示,但我发现了一些让我绊倒的点:

您并行运行 2000 个(或您的评论 6000 个)任务。由于我们在 Winforms 中(WPF 也一样),这些任务中的每一个都将 UI 线程作为同步上下文,这意味着即使你说Task.WhenAll(),它们都必须按顺序执行,因为它们运行在 UI 线程中。

然后在你的代码中你有这个new Random().Next()。这意味着创建了一个新的随机实例,并从当前时间生成种子。这导致了这样一个事实,即您多次产生相同的随机数。当这个数字在您的 95 - 100 范围内时,所有这些任务都会导致Thread.Sleep(1)(而不是await Task.Delay(1)),并且由于您在 UI 线程中,您的 UI 将冻结。

所以在这里我对你的改进:

  • ui upate 代码中分解你的工作马。当您使用时,代码将在另一个线程中执行,但您不能简单地编写,您必须将其包装在调用中以将该方法分派到 UI 线程。CallAPIAsync(dr).ConfigureAwait(false)progressBar1.PerformStep()progressBar1.BeginInvoke()

  • 当您在任务世界中时,请不要使用Thread.Sleep(),因为一个线程负责多个任务。而是使用await Task.Delay(),以便同一线程中的其他任务可以完成他们的工作。

  • 请注意async / awaitUI 应用程序中 using 的含义,以及您的代码是否将在 UI 线程或其他地方运行。了解如何.ConfigureAwait(false)在这些情况下正确使用。

  • 学习正确用法new Random()

另外,你能告诉我每个回调是否在运行下一个回调之前完全运行吗?

这个问题有点复杂,不适合评论。所以这是我的答案。

在您当前的实现中,由于缺少ConfigureAwait(false). 所以你所有的任务都必须由 UI 线程处理。它们按顺序开始,直到到达您的第一个Task.Delay(2000). 在这里,他们排队等待在两秒钟内进行处理。因为排队 2000 个任务比两秒快,你的所有任务或多或少并行地到达这一点。延迟结束后,它们必须由唯一的 UI 线程再次处理。因此它创建一个新Random实例,调用 next 并根据整个过程的结果(注意:UI)线程冻结一毫秒或不冻结。由于您对 Random 类的滥用,您可能会遇到很多异常,如果所有 2000 个任务在 1 毫秒内都遇到异常,这会使您的 UI 冻结 2 秒。


推荐阅读