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 循环迭代已经完成。我希望到目前为止我理解正确。
我有以下选项可以从任务内部报告进度:
使用
IProgress
(我认为这更适合从另一个线程报告进度[例如使用Task.Run
时],或者在通常的异步等待中,如果配置等待为假,则导致等待下面的代码运行单独的线程,否则不会显示进度条移动,因为 ui 线程将被阻止运行回调(在我当前的示例代码中始终在同一 UI 线程上运行)。所以我想以下几点可能是更合适的解决方案。
使任务成为非静态的,以便我可以从任务内访问进度栏并执行
porgressbar1.PerformStep()
。
我注意到的另一件事是
await WhenAll
并不能保证 IProgress
完全执行。
.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;
}
您可以简单地添加一个包装函数:
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
添加所有呼叫后。