在报告异步等待代码与进度条控件的进度时使用 IProgress

Using IProgress when reporting progress for async await code vs progress bar control

private static async Task FuncAsync(DataTable dt, DataRow dr)
{
    try
    {
        await Task.Delay(3000); //assume this is an async http post request that takes 3 seconds to respond
        Thread.Sleep(1000) //assume this is some synchronous code that takes 2 second
    }
    catch (Exception e)
    {
        Thread.Sleep(1000); //assume this is synchronous code that takes 1 second
    }
}
private async void Button1_Click(object sender, EventArgs e)
{
    List<Task> lstTasks = new List<Task>();

    DataTable dt = (DataTable)gridview1.DataSource;

    foreach (DataRow dr in dt.Rows)
    {
        lstTasks.Add(FuncAsync(dr["colname"].ToString());                
    }            

    while (lstTasks.Any())
    {   
        Task finishedTask = await Task.WhenAny(lstTasks);
        lstTasks.Remove(finishedTask);
        await finishedTask;
        progressbar1.ReportProgress();
    }                        
}

假设数据表有 10000 行。

在代码中,单击按钮时,在 for 循环的第一次迭代中,发出异步 api 请求。虽然需要 3 秒,但控制权会立即转到调用方。所以for循环可以进行下一次迭代,依此类推。

当api响应到达时,等待运行s下面的代码作为回调。因此,无论我使用 await WhenAny 还是 WhenAll.

,阻塞 UI 线程和任何未完成的 for 循环迭代都将被延迟,直到回调完成为止

由于存在同步上下文,UI 线程上的所有代码 运行。我可以在 Task.Delay 上执行 ConfigureAwait false,因此回调 运行 在单独的线程上以解锁 ui 线程。

假设在第一次等待 returns 时进行了 1000 次迭代,当第一次迭代等待回调 运行 时,以下迭代将完成完成等待,因此它们的回调将 运行 .如果 configure await 为真,回调将 运行 一个接一个地有效。如果为 false,那么它们将 运行 在单独的线程上并行。

所以我认为我在 while 循环中更新的进度条是不正确的,因为当代码到达 while 块时,大部分初始 for 循环迭代已经完成。我希望到目前为止我理解正确。

我有以下选项来报告任务内部的进度:

  1. using IProgress(我认为这是更多的 suitable 来报告来自另一个线程的进度 [例如当使用 Task.Run] 时,或者在通常情况下async await 如果 configure await 为 false,导致代码低于 await 到 运行 在单独的线程中,否则它不会显示进度条移动,因为 ui 线程将被阻塞 运行ning回调。在我当前的示例代码中,总是 运行s 在同一个 UI 线程上)。所以我在想以下几点可能是更合适的解决方案。

  2. 使任务成为非静态的,这样我就可以从任务中访问进度条并执行 porgressbar1.PerformStep().

我注意到的另一件事是 await WhenAll 不能保证 IProgress 完全执行。

您可以简单地添加一个包装函数:

private IProgress<double> _progress;
private int _jobsFinished = 0;
private int _totalJobs = 1000;

private static async Task FuncAsync()
{
    try
    {
        await Task.Delay(3000); //assume this is an async http post request that takes 3 seconds to respond

        Thread.Sleep(1000); //assume this is some synchronous code that takes 2 second
    }
    catch (Exception e)
    {
        Thread.Sleep(1000); //assume this is synchronous code that takes 1 second
    }
}

private async Task AwaitAndUpdateProgress()
{
    await FuncAsync(); // Can also do Task.Run(FuncAsync) to run on a worker thread
    _jobsFinished++;
    _progress.Report((double) _jobsFinished / _totalJobs);
}

然后 WhenAll 添加所有呼叫后。

.NET 平台本机提供的 IProgress<T> 实现,Progress<T> class, has the interesting characteristic of notifying the captured SynchronizationContext asynchronously, by invoking its Post 方法。此特性有时会导致意外行为。例如,您能猜出下面的代码对 Label1 控件有什么影响吗?

IProgress<string> progress = new Progress<string>(s => Label1.Text = s);

progress.Report("Hello");

Label1.Text = "World";

什么文本最终会写入标签,"Hello""World"?正确答案是:"Hello"。委托 s => Label1.Text = s 是异步调用的,因此它在执行同步调用的 Label1.Text = "World" 行之后运行。

实现 Progress<T> class 的同步版本非常简单。您所要做的就是复制粘贴 Microsoft's source code, rename the class from Progress<T> to SynchronousProgress<T>, and change the line m_synchronizationContext.Post(... to m_synchronizationContext.Send(.... This way every time you invoke the progress.Report method, the call will block until the invocation of the delegate on the UI thread is completed. The unfortunate implication of this is that if the UI thread is blocked for some reason, for example because you used the .Wait() or the .Result to wait synchronously for the task to complete, your application will deadlock.

Progress<T> class 的异步特性在实践中很少成为问题,但如果您想避免考虑它,您可以直接操作 ProgressBar1 控件。毕竟你不是在写一个库,你只是在按钮的事件处理程序中编写代码来发出一些 HTTP 请求。我的建议是忘记 .ConfigureAwait(false) hackery,让你的异步事件处理程序的主要工作流从头到尾留在 UI 线程上。如果您有需要卸载到 ThreadPool 线程的同步阻塞代码,请使用 Task.Run method to offload it. To create your tasks, instead of manually adding tasks to a List<Task>, use the handly LINQ Select operator to project each DataRow to a Task. Also add a reference to the System.Data.DataSetExtensions assembly, so that the DataTable.AsEnumerable extension method becomes available. Finally add a throttler (a SemaphoreSlim),以便您的应用程序有效地利用可用的网络带宽,并且不会使目标负担过重机器:

private async void Button1_Click(object sender, EventArgs e)
{
    Button1.Enabled = false;
    const int maximumConcurrency = 10;
    var throttler = new SemaphoreSlim(maximumConcurrency, maximumConcurrency);

    DataTable dataTable = (DataTable)GridView1.DataSource;
    ProgressBar1.Minimum = 0;
    ProgressBar1.Maximum = dataTable.Rows.Count;
    ProgressBar1.Step = 1;
    ProgressBar1.Value = 0;

    Task[] tasks = dataTable.AsEnumerable().Select(async row =>
    {
        await throttler.WaitAsync();
        try
        {
            await Task.Delay(3000); // Simulate an asynchronous HTTP request
            await Task.Run(() => Thread.Sleep(2000)); // Simulate synchronous code
        }
        catch
        {
            await Task.Run(() => Thread.Sleep(1000)); // Simulate synchronous code
        }
        finally
        {
            throttler.Release();
        }
        ProgressBar1.PerformStep();
    }).ToArray();

    await Task.WhenAll(tasks);
    Button1.Enabled = true;
}