如何同步共享的 IProgress<int>
How to synchronize a shared IProgress<int>
我有一个异步方法 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
,但具有关于通知排序的所需行为?
你的问题是你需要 totalPercentDone
的增量和对 Report
的调用是原子的。
这里使用lock
没有错。毕竟,您需要某种方法使这两个操作成为原子操作。如果你真的不想使用 lock
那么你可以使用 SemaphoireSlim:
async Task DoStuffAsync(IProgress<int> progress)
{
int totalPercentDone = 0;
var semaphore = new SemaphoreSlim(1,1);
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
await semaphore.WaitAsync();
try
{
totalPercentDone += 10;
progress.Report(totalPercentDone);
}
finally
{
semaphore.Release();
}
}
})).ToArray();
await Task.WhenAll(tasks);
}
您可以使用两个单独的整数,并取其中最小的一个,而不是为两个任务使用一个整数。每个Task需要上报100,而不是50。
async Task DoStuffAsync(IProgress<int> progress)
{
int[] totalPercentDone = new int[2];
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
totalPercentDone[n - 1] += 10;
progress.Report(totalPercentDone.Min());
}
})).ToArray();
await Task.WhenAll(tasks);
}
您可以只报告增量并让处理程序处理它们:
private async void Button1_Click(object sender, EventArgs e)
{
TextBox1.Clear();
var totalPercentDone = 0;
var progress = new Progress<int>(x =>
{
totalPercentDone += x;
TextBox1.AppendText($"Progress: {totalPercentDone}\r\n"));
}
await DoStuffAsync(progress);
}
async Task DoStuffAsync(IProgress<int> progress)
{
await Task.WhenAll(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
progress.Report(10);
}
})));
}
这是为了扩展我在问题下的评论
基本上,progress 通常是一个仅向前的值。关于报告 进展 ,您可能永远不需要报告过去取得的进展。即使您这样做,在大多数情况下,客户端/事件处理程序端仍会丢弃收到的此类值。
这里的问题/为什么要同步上报主要是因为你上报的是一个值类型的进度,调用Report(T)
时复制了它的值。
您可以简单地通过报告引用类型实例并取得最新进展来避免锁定:
public class DoStuffProgress
{
private volatile int _percentage;
public int Percentage => _percentage;
internal void IncrementBy(int increment)
{
Interlocked.Add(ref _percentage, increment);
}
}
现在您的代码如下所示:
async Task DoStuffAsync(IProgress<DoStuffProgress> progress)
{
DoStuffProgress totalPercentDone = new DoStuffProgress();
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
totalPercentDone.IncrementBy(10);
// Report reference type object
progress.Report(totalPercentDone);
}
})).ToArray();
await Task.WhenAll(tasks);
}
但是,客户端可能会收到具有重复值的通知:
Progress: 20
Progress: 20
Progress: 40
Progress: 40
Progress: 60
Progress: 60
Progress: 80
Progress: 80
Progress: 90
Progress: 100
但是,值绝不能乱序。
我有一个异步方法 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
,但具有关于通知排序的所需行为?
你的问题是你需要 totalPercentDone
的增量和对 Report
的调用是原子的。
这里使用lock
没有错。毕竟,您需要某种方法使这两个操作成为原子操作。如果你真的不想使用 lock
那么你可以使用 SemaphoireSlim:
async Task DoStuffAsync(IProgress<int> progress)
{
int totalPercentDone = 0;
var semaphore = new SemaphoreSlim(1,1);
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
await semaphore.WaitAsync();
try
{
totalPercentDone += 10;
progress.Report(totalPercentDone);
}
finally
{
semaphore.Release();
}
}
})).ToArray();
await Task.WhenAll(tasks);
}
您可以使用两个单独的整数,并取其中最小的一个,而不是为两个任务使用一个整数。每个Task需要上报100,而不是50。
async Task DoStuffAsync(IProgress<int> progress)
{
int[] totalPercentDone = new int[2];
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
totalPercentDone[n - 1] += 10;
progress.Report(totalPercentDone.Min());
}
})).ToArray();
await Task.WhenAll(tasks);
}
您可以只报告增量并让处理程序处理它们:
private async void Button1_Click(object sender, EventArgs e)
{
TextBox1.Clear();
var totalPercentDone = 0;
var progress = new Progress<int>(x =>
{
totalPercentDone += x;
TextBox1.AppendText($"Progress: {totalPercentDone}\r\n"));
}
await DoStuffAsync(progress);
}
async Task DoStuffAsync(IProgress<int> progress)
{
await Task.WhenAll(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
progress.Report(10);
}
})));
}
这是为了扩展我在问题下的评论
基本上,progress 通常是一个仅向前的值。关于报告 进展 ,您可能永远不需要报告过去取得的进展。即使您这样做,在大多数情况下,客户端/事件处理程序端仍会丢弃收到的此类值。
这里的问题/为什么要同步上报主要是因为你上报的是一个值类型的进度,调用Report(T)
时复制了它的值。
您可以简单地通过报告引用类型实例并取得最新进展来避免锁定:
public class DoStuffProgress
{
private volatile int _percentage;
public int Percentage => _percentage;
internal void IncrementBy(int increment)
{
Interlocked.Add(ref _percentage, increment);
}
}
现在您的代码如下所示:
async Task DoStuffAsync(IProgress<DoStuffProgress> progress)
{
DoStuffProgress totalPercentDone = new DoStuffProgress();
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
totalPercentDone.IncrementBy(10);
// Report reference type object
progress.Report(totalPercentDone);
}
})).ToArray();
await Task.WhenAll(tasks);
}
但是,客户端可能会收到具有重复值的通知:
Progress: 20
Progress: 20
Progress: 40
Progress: 40
Progress: 60
Progress: 60
Progress: 80
Progress: 80
Progress: 90
Progress: 100
但是,值绝不能乱序。