【发布时间】:2020-07-30 17:23:15
【问题描述】:
我有一个异步方法DoStuffAsync,它使用Task.Run 生成两个任务,两个任务都使用一个IProgress<int> 对象报告它们的进度。从用户的角度来看,只有一个操作,因此显示两个进度条(每个Task 一个)没有任何意义。这就是共享IProgress<int> 的原因。问题是有时 UI 以错误的顺序接收进度通知。这是我的代码:
private async void Button1_Click(object sender, EventArgs e)
{
TextBox1.Clear();
var progress = new Progress<int>(x => TextBox1.AppendText($"Progress: {x}\r\n"));
await DoStuffAsync(progress);
}
async Task DoStuffAsync(IProgress<int> progress)
{
int totalPercentDone = 0;
Task[] tasks = Enumerable.Range(1, 2).Select(n => Task.Run(async () =>
{
for (int i = 0; i < 5; i++)
{
await Task.Delay(100); // Simulate an I/O operation
var localPercentDone = Interlocked.Add(ref totalPercentDone, 10);
progress.Report(localPercentDone);
}
})).ToArray();
await Task.WhenAll(tasks);
}
大多数时候通知的顺序是正确的,但有时它们不是:
这会导致ProgressBar 控件(上面的屏幕截图中未显示)笨拙地来回跳跃。
作为临时解决方案,我在DoStuffAsync 方法中添加了一个lock,其中包括对IProgress.Report 方法的调用:
async Task DoStuffAsync(IProgress<int> progress)
{
int totalPercentDone = 0;
object locker = new object();
Task[] tasks = Enumerable.Range(1, 2).Select(n => Task.Run(async () =>
{
for (int i = 0; i < 5; i++)
{
await Task.Delay(100); // Simulate an I/O operation
lock (locker)
{
totalPercentDone += 10;
progress.Report(totalPercentDone);
};
}
})).ToArray();
await Task.WhenAll(tasks);
}
虽然这解决了问题,但它让我感到焦虑,因为我在持有lock 的同时调用任意代码。 DoStuffAsync 方法实际上是库的一部分,可以使用任何 IProgress<int> 实现作为参数来调用。这打开了死锁场景的可能性。有没有更好的方法来实现DoStuffAsync 方法,而不使用lock,但具有关于通知排序的所需行为?
【问题讨论】:
-
其实有多种选择。您有 2 个任务和 100% 条 - 您可以只将 Max percent 传递到 Report 中(如果它们本质上相同,这将是最简单的方法),或者将它们分成 50% 用于一项任务,50% 用于其他任务,并且每个其中一个为它的单独实例做出贡献,您可以听听这两者的组合。
-
我不认为锁是个麻烦。甚至涉及到外部操作,锁定的对象是在本地声明的,因此没有自定义 IProgress 实现可以访问它。只有在自定义 IProgress 实现挂起的情况下才可能出现死锁,这可能会导致同样的问题(任务不工作)。
-
如果您将
totalPercentDone声明为字段,并且在DoStuffAsync中,您只需:await Task.WhenAll(Enumerable.Range(1, 2).Select(n => DoWorkAsync(progress)));(在按钮中重置totalPercentDone。单击)。DoWorkAsync()当然是worker方法。在那里你可以拥有:progress.Report(Interlocked.Add(ref totalPercentDone, 10));。如果您反复按下按钮,它也应该可以工作(但结果会累积。需要禁用该按钮)。 -
我在 UWP 应用程序(类似于 WinForm 应用程序)中处理此问题的方式只是忽略了
ProgressChanged中的较小值。对我来说,保持/显示过去取得的进展是没有意义的。而且我可以在锁定的上下文中使用更少的代码。也许你这里有有效的案例,你得到了很好的答案。 -
@weichch 这绝对是一个可靠的选择。我想避免将订单保留的负担转移到客户端。理想情况下,客户端代码(
DoStuffAsync方法的调用者)应该只订阅Progress.ProgressChanged事件,然后一切都应该按预期工作。
标签: c# winforms async-await task-parallel-library iprogress