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

问题描述 投票:0回答:2
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 响应到达时,await 下面的代码将作为回调运行。因此,无论我使用await

WhenAny
还是
WhenAll
,阻塞UI线程和任何不完整的for循环迭代都将被延迟,直到回调完成。

由于同步上下文的存在,所有代码都在 UI 线程上运行。我可以在

ConfigureAwait
上执行
false
Task.Delay
,以便回调在单独的线程上运行,以便解锁 ui 线程。

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

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

我有以下选项可以从任务内部报告进度:

  1. 使用

    IProgress
    (我认为这更适合从另一个线程报告进度[例如使用
    Task.Run
    时],或者在通常的异步等待中,如果配置等待为假,则导致等待下面的代码运行单独的线程,否则不会显示进度条移动,因为 ui 线程将被阻止运行回调(在我当前的示例代码中始终在同一 UI 线程上运行)。所以我想以下几点可能是更合适的解决方案。

  2. 使任务成为非静态的,以便我可以从任务内访问进度栏并执行

    porgressbar1.PerformStep()

我注意到的另一件事是

await WhenAll
并不能保证
IProgress
完全执行。

c# multithreading asynchronous async-await task-parallel-library
2个回答
12
投票

.NET 平台原生提供的

IProgress<T>
实现,即
Progress<T>
类,具有通过调用其
SynchronizationContext
 方法异步通知捕获的 
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>
类的同步版本非常简单。您所要做的就是复制粘贴 Microsoft 的源代码,将类从
Progress<T>
重命名为
SynchronousProgress<T>
,并将行
m_synchronizationContext.Post(...
更改为
m_synchronizationContext.Send(...
。这样,每次调用
progress.Report
方法时,调用都会阻塞,直到 UI 线程上的委托调用完成。可以在here找到简化的实现。不幸的是,如果 UI 线程由于某种原因被阻塞,例如因为您使用
.Wait()
.Result
同步等待任务完成,您的应用程序将死锁

Progress<T>
类的异步特性在实践中很少会出现问题,但如果您想避免考虑它,您可以直接操作
ProgressBar1
控件。毕竟您不是在编写库,您只是在按钮的事件处理程序中编写代码来发出一些 HTTP 请求。我的建议是忘记
.ConfigureAwait(false)
黑客行为,只让异步事件处理程序的主要工作流程从开始到结束都保留在 UI 线程上。如果您有需要卸载到
ThreadPool
线程的同步阻塞代码,请使用
Task.Run
方法来卸载它。要创建任务,无需手动将任务添加到
List<Task>
,而是使用方便的 LINQ
Select
运算符将每个
DataRow
投影到
Task
。还要添加对
System.Data.DataSetExtensions
程序集的引用,以便
DataTable.AsEnumerable
扩展方法变得可用。最后添加一个节流器(
SemaphoreSlim
),以便您的应用程序有效地利用可用的网络带宽,并且不会使目标机器负担过重:

private async void Button1_Click(object sender, EventArgs e)
{
    Button1.Enabled = false;
    const int maximumConcurrency = 10;
    SemaphoreSlim throttler = new(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;
}

0
投票

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

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
添加所有呼叫后。

© www.soinside.com 2019 - 2024. All rights reserved.