C# 如何同步共享的IProgress<;int>;

C# 如何同步共享的IProgress<;int>;,c#,winforms,async-await,task-parallel-library,iprogress,C#,Winforms,Async Await,Task Parallel Library,Iprogress,我有一个异步方法DoStuffAsync,它通过Task.Run生成两个任务,两个任务都使用单个对象报告其进度。从用户的角度来看,只有一个操作,因此显示两个进度条(每个任务一个)没有任何意义。这就是共享i进程的原因。问题是UI有时会以错误的顺序接收进度通知。这是我的密码: private async void Button1_Click(object sender, EventArgs e) { TextBox1.Clear(); var progress = new Progr

我有一个异步方法
DoStuffAsync
,它通过
Task.Run
生成两个任务,两个任务都使用单个对象报告其进度。从用户的角度来看,只有一个操作,因此显示两个进度条(每个
任务一个)没有任何意义。这就是共享
i进程的原因。问题是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);
}

虽然这解决了问题,但它让我感到焦虑,因为我在持有
锁时调用了任意代码。
DoStuffAsync
方法实际上是库的一部分,可以使用任意
IProgress
实现作为参数进行调用。这为死锁场景提供了可能性。是否有更好的方法来实现
DoStuffAsync
方法,而不使用
锁,而是使用所需的通知顺序行为?

您的问题是需要增加
totalPercentDone
并且对
Report
的调用是原子的

在这里使用
锁没有什么问题。毕竟,您需要某种方法使这两个操作原子化。如果您真的不想使用
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);
}
async Task DoStuffAsync(IProgress progress)
{
int totalPercentDone=0;
var信号量=新信号量lim(1,1);
Task[]tasks=Enumerable.Range(1,2)。选择(n=>Task.Run(async()=>
{
对于(int i=0;i<5;i++)
{
等待任务。延迟(100);//模拟I/O操作
wait semaphore.WaitAsync();
尝试
{
总完成百分比+=10;
进度报告(完成百分比);
}
最后
{
semaphore.Release();
}
}
})).ToArray();
等待任务。何时(任务);
}

您可以使用两个单独的整数,并取其中最小的整数,而不是两个任务都使用一个整数。每个任务需要报告到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);
}
async Task DoStuffAsync(IProgress progress)
{
int[]totalPercentDone=新int[2];
Task[]tasks=Enumerable.Range(1,2)。选择(n=>Task.Run(async()=>
{
对于(int i=0;i<5;i++)
{
等待任务。延迟(100);//模拟I/O操作
总完成百分比[n-1]+=10;
progress.Report(totalPercentDone.Min());
}
})).ToArray();
等待任务。何时(任务);
}

您只需报告增量并让处理程序处理它们:

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);
        }
    })));
}
private async void按钮1\u单击(对象发送方,事件参数e)
{
TextBox1.Clear();
var totalPercentDone=0;
变量进度=新进度(x=>
{
totalPercentDone+=x;
TextBox1.AppendText($“进度:{totalPercentDone}\r\n”);
}
等待Dostufasync(进度);
}
异步任务同步(IProgress进程)
{
wait Task.WhenAll(Enumerable.Range(1,2)。选择(n=>Task.Run(async()=>
{
对于(int i=0;i<5;i++)
{
等待任务。延迟(100);//模拟I/O操作
进度报告(10);
}
})));
}
这是对我的评论的延伸

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

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

但是,这些值不应该出现顺序错误。

实际上有多个选项。您有2个任务和100%条-您可以只取传递到报告中的最大百分比(如果它们本质相同,这将是最简单的方法),或者将它们分成50%用于一个任务,50%用于另一个任务,并且它们中的每一个都有助于它的单独实例,并且您可以侦听这两个的组合。我不认为锁定是一个问题。即使涉及外部操作,锁定的对象也是在本地声明的,因此没有自定义IProgress实现可以访问它。只有在自定义IProgress实现挂起的情况下才可能出现死锁,这可能会导致同样的问题(任务不工作)。如果您将
totalPercentDone
声明为字段,并且在
Dostufasync
中,您只需:
等待任务。whalll(可枚举的.Range(1,2)。选择(n=>DoWorkAsync(progress))(在按钮中重置
totalPercentDone
。单击)
doworksync()
当然是辅助方法。您可以在那里看到:
progress.Report(Interlocked.Add(ref totalPercentDone,10))。如果你反复按下按钮,它也会起作用(但结果会累积。需要禁用该按钮)。我在UWP应用程序(类似于WinForm应用程序)中处理此问题的方法是忽略
ProgressChanged
中较小的值。对我来说,保持/展示过去取得的进步毫无意义。我可以在锁定的上下文中使用更少的代码。也许你在这里有有效的案例,并且你得到了很好的答案。@weichch这绝对是一个可靠的选择。不过,我希望避免将订单保存的负担转移到客户端。理想情况下,客户端代码(调用
DoStuffAsync
方法)应该只订阅
Progress.ProgressChanged
事件,然后一切都应该按预期工作。感谢Random提供的答案。我测试了你的解决方案。不幸的是,它在t中生成通知
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