IProgress<T> 同步化

19

我在C#中有以下代码:

public static void Main()
{
    var result = Foo(new Progress<int>(i =>
        Console.WriteLine("Progress: " + i)));

    Console.WriteLine("Result: " + result);            
    Console.ReadLine();
}

static int Foo(IProgress<int> progress)
{
    for (int i = 0; i < 10; i++)
        progress.Report(i);

    return 1001;
}

主要程序的一些输出内容如下:

第一次运行:

Result: 1001
Progress: 4
Progress: 6
Progress: 7
Progress: 8
Progress: 9
Progress: 3
Progress: 0
Progress: 1
Progress: 5
Progress: 2

第二次运行:

Progress: 4
Progress: 5
Progress: 6
Progress: 7
Progress: 8
Progress: 9
Progress: 0
Progress: 1
Progress: 2
Result: 1001
Progress: 3

每次运行的输出都不同。我应该如何同步这些方法,以便按照0、1、...、9报告它们的进度,最后显示结果为1001。我希望输出如下:

Progress: 0
.
.
.
Progress: 9
Result: 1001
4个回答

17

Progress<>类使用SynchronizationContext.Current属性在Post()方法中进行进度更新。这样做是为了确保在程序的UI线程上触发ProgressChanged事件,以便安全地更新UI。例如ProgressBar.Value属性需要安全更新。

控制台应用程序的问题在于它没有同步提供程序,不像Winforms或WPF应用程序。Synchronization.Current属性具有默认提供程序,其Post()方法在线程池线程上运行。没有任何互锁,哪个TP线程首先报告其更新是完全不可预测的。也没有什么好的互锁方式。

在此处不要使用Progress类,没有意义。 在控制台模式应用程序中,您不会遇到UI线程安全问题,Console类已经是线程安全的。修复:

static int Foo()
{
    for (int i = 0; i < 10; i++)
        Console.WriteLine("Progress: {0}", i);

    return 1001;
}

我正在设计一个将用于控制台应用程序,潜在地还包括GUI应用程序(包括Web应用程序)的库。我考虑另一种选择是使用委托Action<int>,这在此场景下可以工作,但我不确定是否这是最佳解决方案,如果我的库被GUI应用程序使用,你认为呢? - danze
你不能为一个你不了解的应用程序做出这个决定,你不知道它的线程要求,也不能假设你正在任何特定的线程上使用。所以最好不要这样做,触发一个事件,让客户端应用程序来处理它。 - Hans Passant
1
实际上,你写的代码并不那么糟糕,正如汉斯所说,你不能在你的库中做出决定,因此使用接口IProgress<int>绝对是个好主意。从GUI应用程序中,你可以使用Progress实现,从控制台应用程序中,你可以使用自己的实现。如果你想要,你的IProgress实现可以只包装Action<int>,这样它就能按照你想要的方式工作了。我更喜欢这种方式来处理事件,IProgress似乎是现在的标准。 - Lukas K
正如其他答案中所说,您可以自己编写控制台实现,并将其提供给客户/用户作为示例,或随产品一起发布。请参见:https://dev59.com/l2Ml5IYBdhLWcg3w966L#32933479 以获取基本示例。 - Yves Schelpe
大有帮助,即使我没有在控制台应用程序中使用它。我的是一个VSTO Word插件,由于某种原因,VSTO也没有设置同步上下文,导致了奇怪的死锁情况。手动设置上下文立即解决了这个问题。 - dotNET

9
Hans' answer所述,Progress<T>的.NET实现使用SynchronizationContext.Post发送请求。您可以像Yves' answer那样直接报告,或者使用SynchronizationContext.Send使请求在接收方处理之前阻塞。
由于Reference Source可用,因此实现它就像复制源代码并将Post更改为Send以及将SynchronizationContext.CurrentNoFlow更改为SynchronizationContext.Current一样简单,因为CurrentNoFlow是一个内部属性。
/// <summary>
/// Provides an IProgress{T} that invokes callbacks for each reported progress value.
/// </summary>
/// <typeparam name="T">Specifies the type of the progress report value.</typeparam>
/// <remarks>
/// Any handler provided to the constructor or event handlers registered with
/// the <see cref="ProgressChanged"/> event are invoked through a 
/// <see cref="System.Threading.SynchronizationContext"/> instance captured
/// when the instance is constructed.  If there is no current SynchronizationContext
/// at the time of construction, the callbacks will be invoked on the ThreadPool.
/// </remarks>
public class SynchronousProgress<T> : IProgress<T>
{
    /// <summary>The synchronization context captured upon construction.  This will never be null.</summary>
    private readonly SynchronizationContext m_synchronizationContext;
    /// <summary>The handler specified to the constructor.  This may be null.</summary>
    private readonly Action<T> m_handler;
    /// <summary>A cached delegate used to post invocation to the synchronization context.</summary>
    private readonly SendOrPostCallback m_invokeHandlers;

    /// <summary>Initializes the <see cref="Progress{T}"/>.</summary>
    public SynchronousProgress()
    {
        // Capture the current synchronization context.  "current" is determined by Current.
        // If there is no current context, we use a default instance targeting the ThreadPool.
        m_synchronizationContext = SynchronizationContext.Current ?? ProgressStatics.DefaultContext;
        Contract.Assert(m_synchronizationContext != null);
        m_invokeHandlers = new SendOrPostCallback(InvokeHandlers);
    }

    /// <summary>Initializes the <see cref="Progress{T}"/> with the specified callback.</summary>
    /// <param name="handler">
    /// A handler to invoke for each reported progress value.  This handler will be invoked
    /// in addition to any delegates registered with the <see cref="ProgressChanged"/> event.
    /// Depending on the <see cref="System.Threading.SynchronizationContext"/> instance captured by 
    /// the <see cref="Progress"/> at construction, it's possible that this handler instance
    /// could be invoked concurrently with itself.
    /// </param>
    /// <exception cref="System.ArgumentNullException">The <paramref name="handler"/> is null (Nothing in Visual Basic).</exception>
    public SynchronousProgress(Action<T> handler) : this()
    {
        if (handler == null) throw new ArgumentNullException("handler");
        m_handler = handler;
    }

    /// <summary>Raised for each reported progress value.</summary>
    /// <remarks>
    /// Handlers registered with this event will be invoked on the 
    /// <see cref="System.Threading.SynchronizationContext"/> captured when the instance was constructed.
    /// </remarks>
    public event EventHandler<T> ProgressChanged;

    /// <summary>Reports a progress change.</summary>
    /// <param name="value">The value of the updated progress.</param>
    protected virtual void OnReport(T value)
    {
        // If there's no handler, don't bother going through the [....] context.
        // Inside the callback, we'll need to check again, in case 
        // an event handler is removed between now and then.
        Action<T> handler = m_handler;
        EventHandler<T> changedEvent = ProgressChanged;
        if (handler != null || changedEvent != null)
        {
            // Post the processing to the [....] context.
            // (If T is a value type, it will get boxed here.)
            m_synchronizationContext.Send(m_invokeHandlers, value);
        }
    }

    /// <summary>Reports a progress change.</summary>
    /// <param name="value">The value of the updated progress.</param>
    void IProgress<T>.Report(T value) { OnReport(value); }

    /// <summary>Invokes the action and event callbacks.</summary>
    /// <param name="state">The progress value.</param>
    private void InvokeHandlers(object state)
    {
        T value = (T)state;

        Action<T> handler = m_handler;
        EventHandler<T> changedEvent = ProgressChanged;

        if (handler != null) handler(value);
        if (changedEvent != null) changedEvent(this, value);
    }
}

/// <summary>Holds static values for <see cref="Progress{T}"/>.</summary>
/// <remarks>This avoids one static instance per type T.</remarks>
internal static class ProgressStatics
{
    /// <summary>A default synchronization context that targets the ThreadPool.</summary>
    internal static readonly SynchronizationContext DefaultContext = new SynchronizationContext();
}

6

正如其他回答多次指出的那样,它是由 Progress<T> 的实现方式导致的。您可以向客户(库的用户)提供示例代码或控制台项目的 IProgress<T> 实现。这很基础,但应该足够。

public class ConsoleProgress<T> : IProgress<T>
{
    private Action<T> _action;

    public ConsoleProgress(Action<T> action) {
        if(action == null) {
            throw new ArgumentNullException(nameof(action));
        }

        _action = action;
    }

    public void Report(T value) {
        _action(value);
    }
}

1
这是一个关于Progress<T>的线程问题。您需要编写自己的IProgress<T>实现来获得所需内容。
然而,这个场景已经告诉您一些重要的事情,虽然在这个例子中,您只是简单地使用Console.Writeline语句,但在真实场景中,由于某些报告需要更长或更短的时间,因此可能以其他顺序报告,所以我认为您不应该依赖顺序。

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