如何同步共享的 IProgress<int>

The*_*ias 2 c# winforms task-parallel-library async-await iprogress

我有一个异步方法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);
}
Run Code Online (Sandbox Code Playgroud)

大多数情况下,通知的顺序是正确的,但有时不是:

截图

这会导致ProgressBar控件(未在上面的屏幕截图中显示)笨拙地来回跳跃。

作为临时解决方案,我lockDoStuffAsync方法内部添加了一个,其中包括对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);
}
Run Code Online (Sandbox Code Playgroud)

虽然这解决了问题,但它让我感到焦虑,因为我在持有lock. 该DoStuffAsync方法实际上是库的一部分,可以使用任何IProgress<int>实现作为参数调用。这打开了死锁场景的可能性。是否有更好的方法来实现该DoStuffAsync方法,而不使用 a lock,但具有有关通知排序的所需行为?

Sea*_*ean 5

您的问题是您需要totalPercentDoneAND 调用的增量Report是原子的。

lock这里使用 a 没有错。毕竟,您需要某种方式使这两个操作原子化。如果你真的不想使用,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);
}
Run Code Online (Sandbox Code Playgroud)

  • `Progress.IProgress.Report` 不会等到处理程序被调用;它仅将进度报告“排队”到创建“Progress”时捕获的 SyncCtx。WinForms SyncCtx 按顺序处理它们。如果没有 FIFO 排队 SyncCtx(即,在一般情况下),报告仍然可能是无序的。 (3认同)