var*_*ble 4 c# multithreading asynchronous task-parallel-library async-await
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
}
}
Run Code Online (Sandbox Code Playgroud)
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();
}
}
Run Code Online (Sandbox Code Playgroud)
假设数据表有 10000 行。
在代码中,单击按钮时,在 for 循环的第一次迭代时,会发出异步 api 请求。虽然需要 3 秒钟,但控制权会立即转到调用者手中。所以for循环可以进行下一次迭代,依此类推。
当 api 响应到达时,await 下面的代码将作为回调运行。因此,无论我是否使用 waitWhenAny或 ,阻塞 UI 线程和任何不完整的 for 循环迭代都将被延迟,直到回调完成WhenAll。
由于同步上下文的存在,所有代码都在 UI 线程上运行。我可以这样做ConfigureAwait false,Task.Delay以便回调在单独的线程上运行,以便解锁 ui 线程。
假设当第一个等待返回时进行了 1000 次迭代,并且当第一个迭代等待回调运行时,以下迭代将完成完整的等待,因此它们的回调将运行。如果配置等待为真,回调将有效地一个接一个地运行。如果为 false,那么它们将在单独的线程上并行运行。
所以我认为我在 while 循环中更新的进度条是不正确的,因为当代码到达 while 块时,大部分初始 for 循环迭代已经完成。我希望到目前为止我的理解是正确的。
我有以下选项可以从任务内部报告进度:
使用IProgress(我认为这更适合报告另一个线程的进度[例如使用时Task.Run],或者在通常的异步等待中,如果配置等待为假,则导致等待下面的代码在单独的线程中运行,否则它将不会显示进度条移动,因为 ui 线程将被阻止运行回调。在我当前的示例中,代码始终在同一个 UI 线程上运行。所以我想以下几点可能是更合适的解决方案。
使任务成为非静态,以便我可以从任务内访问进度栏并执行porgressbar1.PerformStep().
我注意到的另一件事是,这await WhenAll并不能保证IProgress完全执行。
The*_*ias 10
IProgress<T>.NET 平台本机提供的实现(即该类)具有一个有趣的特性,即通过调用其方法来异步Progress<T>通知捕获的内容。此特性有时会导致意外行为。例如,你能猜出下面的代码对控件有什么影响吗?SynchronizationContextPostLabel1
IProgress<string> progress = new Progress<string>(s => Label1.Text = s);
progress.Report("Hello");
Label1.Text = "World";
Run Code Online (Sandbox Code Playgroud)
最终将向标签写入什么文本,"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 线程上的委托调用完成。不幸的是,如果 UI 线程由于某种原因被阻塞,例如因为您使用.Wait()或 来.Result同步等待任务完成,您的应用程序将死锁。
类的异步特性Progress<T>在实践中很少成为问题,但如果您想避免考虑它,可以直接操作控件ProgressBar1。毕竟您不是在编写库,您只是在按钮的事件处理程序中编写代码来发出一些 HTTP 请求。我的建议是忘记.ConfigureAwait(false)黑客行为,只让异步事件处理程序的主要工作流程从开始到结束都保留在 UI 线程上。如果您有需要卸载到ThreadPool线程的同步阻塞代码,请使用该Task.Run方法将其卸载。要创建任务,无需手动将任务添加到List<Task>,而是使用手动 LINQSelect运算符将每个任务投影DataRow到Task。还要添加对程序集的引用System.Data.DataSetExtensions,以便DataTable.AsEnumerable扩展方法可用。最后添加一个节流器 (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;
}
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
12037 次 |
| 最近记录: |