Progress<T>和Action<T>有什么不同?

17

我一直在使用 Progress<T>,想知道它是否可以被 Action<T> 替换。

在下面的代码中,使用它们来报告进度,即 ReportWithProgress()ReportWithAction(),对我来说没有任何明显的区别。如何增加 progressBar1,如何在输出窗口中写入字符串,它们看起来都是相同的。

// WinForm application with progressBar1

private void HeavyIO()
{
    Thread.Sleep(20); // assume heavy IO
}

private async Task ReportWithProgress()
{
    IProgress<int> p = new Progress<int>(i => progressBar1.Value = i);

    for (int i = 0; i <= 100; i++)
    {
        await Task.Run(() => HeavyIO()); 
        Console.WriteLine("Progress : " + i);
        p.Report(i);
    }
}

private async Task ReportWithAction()
{
    var a = new Action<int>(i => progressBar1.Value = i);

    for (int i = 0; i <= 100; i++)
    {
        await Task.Run(() => HeavyIO());
        Console.WriteLine("Action : " + i);
        a(i);
    }
} 

但是Progress<T>不能是重复造轮子。它被实现的原因应该是有道理的。谷歌搜索“c# Progress vs Action”并没有给我太多帮助。进度与操作有何不同?


1
我建议你将 HeavyIO 更改为 async Task HeavyIO() { await Task.Delay(20); },这样至少你会调用一个任务。 - Camilo Terevinto
3
Progress<T>会在构造时所处的上下文中调用该操作,这使您可以与UI交互而不需要繁琐的调用代码。 - DavidG
你在 Progress 的文档中找到了什么信息,它为什么无法回答你的问题? - Servy
在不同的线程中调用 progressBar1.Value = i 会导致可怕的 "跨线程操作无效" 异常。 - vgru
请注意,Progress<T>是一个,而Action<T>仅仅是一个委托 - Matthew Watson
2个回答

19

在不同的线程中调用progressBar1.Value = i会导致令人恐惧的"跨线程操作无效"异常。另一方面,Progress类将事件分派到在构造时捕获的同步上下文

// simplified code, check reference source for actual code

void IProgress<T>.Report(T value)
{
    // post the processing to the captured sync context
    m_synchronizationContext.Post(InvokeHandlers, value);
}

private void InvokeHandlers(object state)
{
    // invoke the handler passed through the constructor
    m_handler?.Invoke((T)state);

    // invoke the ProgressChanged event handler
    ProgressChanged?.Invoke(this, (T)state);
}

这可以确保所有进度条、标签和其他UI元素的更新都在(唯一)GUI线程上完成。

因此,在UI线程上调用的方法中,只有在后台线程之外实例化Progress类才是有意义的:

void Button_Click(object sender, EventArgs e)
{
    // since this is a UI event, instantiating the Progress class
    // here will capture the UI thread context
    var progress = new Progress<int>(i => progressBar1.Value = i);

    // pass this instance to the background task
    Task.Run(() => ReportWithProgress(progress));
}

async Task ReportWithProgress(IProgress<int> p)
{
    for (int i = 0; i <= 100; i++)
    {
        await Task.Run(() => HeavyIO());
        Console.WriteLine("Progress : " + i);
        p.Report(i);
    }
}

@ChrFin:好的,这是您的窗体控件绑定的“唯一”线程。 - vgru

5
区别在于使用Progress<T>时,您可以有多个侦听器监听进度,而且Progress<T>在创建实例时会捕获SynchonizationContext,因此如果在GUI线程中创建,则不需要调用到GUI线程。
您也可以向Action<T>添加多个侦听器(感谢@Servy指出),但是每个侦听器都在调用该操作的线程中执行。

考虑以下扩展示例,在此示例中,Progress<T>将起作用,但Action<T>将会抛出异常

private async Task ReportWithProgress()
{
    var p = new Progress<int>(i => progressBar1.Value = i);
    p.ProgressChanged += (s, e) => progressBar2.Value = e;

    Task.Run(() => 
        {
            for (int i = 0; i <= 100; i++)
            {
                await Task.Run(() => HeavyIO()); 
                Console.WriteLine("Progress : " + i);
                ((IProgress<int>)p).Report(i);
            }
        });
}

private async Task ReportWithAction()
{
    var a = new Action<int>(i => progressBar1.Value = i);
    a += i => progressBar2.Value = i;

    Task.Run(() => 
        {
            for (int i = 0; i <= 100; i++)
            {
                await Task.Run(() => HeavyIO());
                Console.WriteLine("Action : " + i);
                a(i);
            }
        });
} 

3
Action<T> 是一个多路广播委托。在调用它时,可以调用任意数量的方法。 - Servy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接