2

我有一个异步方法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控件(上面的屏幕截图中未显示)笨拙地来回跳跃。

作为临时解决方案,我在方法lock内部添加了一个DoStuffAsync,其中包括方法的调用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方法,而不使用 a lock,但具有关于通知排序的所需行为?

4

4 回答 4

5

你的问题是你需要增加totalPercentDoneAND 调用Report是原子的。

在这里使用 a 没有任何问题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);
}
于 2020-04-17T09:10:08.137 回答
4

您可以只报告增量并让处理处理它们:

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);
        }
    })));
}
于 2020-04-17T12:35:05.773 回答
3

您可以使用两个单独的 int,并取其中最小的一个,而不是对两个任务都使用一个 int。每个任务需要报告到 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);
}
于 2020-04-17T09:10:16.143 回答
3

这是为了扩展我的评论

基本上,进度通常是一个仅向前的值。关于报告进度,您可能永远不需要报告过去取得的进展。即使您这样做,在大多数情况下,客户端/事件处理程序端仍会丢弃收到的此类值。

这里的问题/为什么需要同步报告主要是因为您正在报告值类型的进度,其值在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

但是,这些值永远不应该乱序。

于 2020-04-18T06:53:19.540 回答