Синхронизация IProgress <T>
У меня есть в С#
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;
}
Некоторые выходы Main:
Первый запуск:
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
Ответы
Ответ 1
Класс Progress < > использует свойство SynchronizationContext.Current для Post() обновления выполнения. Это было сделано для обеспечения того, чтобы событие ProgressChanged запускалось в потоке пользовательского интерфейса программы, поэтому можно безопасно обновлять интерфейс. Необходимо безопасно обновить, скажем, свойство ProgressBar.Value.
Проблема с консольным приложением заключается в том, что у него нет поставщика синхронизации. Не похоже на Winforms или приложение WPF. Свойство Synchronization.Current имеет поставщик по умолчанию, его метод Post() работает в потоке threadpool. Без какой-либо блокировки вообще, какой поток TP получает сообщение о своем обновлении, сначала совершенно непредсказуем. Невозможно также блокировать.
Просто не используйте класс Progress здесь, нет смысла. У вас нет проблемы безопасности потока пользовательского интерфейса в приложении с консольным режимом, класс консоли уже потокобезопасен. Fix:
static int Foo()
{
for (int i = 0; i < 10; i++)
Console.WriteLine("Progress: {0}", i);
return 1001;
}
Ответ 2
Как сказано в ответе Hans, реализация .NET Progress<T>
использует SynchronizationContext.Post
, чтобы отправить свои запросы. Вы можете сделать это напрямую, как в ответ Yves ', или использовать 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 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 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();
}
Ответ 3
Как указывалось несколько раз ранее другими ответами, это связано с тем, как 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);
}
}
Ответ 4
Это проблема с потоками в написании Progress<T>
. Вам нужно написать собственную реализацию IProgress<T>
, чтобы получить то, что вам нужно.
Однако этот сценарий уже говорит вам о чем-то важном, хотя в этом примере вы просто делаете простые инструкции Console.Writeline
, в реальных сценариях некоторые отчеты могут сообщаться в каком-то другом порядке из-за того, что они занимают больше времени или меньше, поэтому в моем вы не должны полагаться на заказ в любом случае.