System.Progress не запускает события в основном потоке после отображения диалогового окна WinForms

У меня есть диалоговое окно WPF, в котором отображается индикатор выполнения и фоновая задача (System.Threading.Tasks.Task), который предоставляет поток обновлений о ходе выполнения, которые необходимо передать в индикатор выполнения. Посредником между ними является объект System.Progress<T>.

Все это отлично работает при «нормальных» обстоятельствах:

  • Фоновая задача вызывает System.IProgress.Report() в некотором потоке X, который не является основным потоком.
  • Объект System.Progress выполняет свою внутреннюю магию, переключая потоки.
  • Объект System.Progress запускает событие ProgressChanged в основном потоке.
  • Элемент управления индикатором прогресса обновляется в основном потоке (который владеет элементом управления)

Теперь, если я открою любое диалоговое окно WinForms, затем закрою его, а затем запущу фоновую задачу, System.Progress внезапно вызовет событие ProgressChanged не в основном потоке, а в каком-то потоке Y, который не является основным потоком. . Это, конечно, приводит к InvalidOperationException, потому что обработчик событий пытается обновить элемент управления индикатором выполнения WPF в потоке, отличном от того, которому принадлежит элемент управления.

Я заметил, что документы для System.Progress говорят, что:

[...] обработчики событий, зарегистрированные с событием ProgressChanged, вызываются через SynchronizationContext экземпляр захвачен при создании экземпляра. Если на момент конструкции, обратные вызовы будут вызываться для ThreadPool. .

Кажется, это соответствует тому, что я могу наблюдать, потому что так выглядит нижняя часть стека вызовов, когда System.Progress запускает свое событие в плохом случае:

[...]
bei System.Progress`1.InvokeHandlers(Object state)
bei System.Threading.QueueUserWorkItemCallback.WaitCallback_Context(Object state)
bei System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
bei System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
bei System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem()
bei System.Threading.ThreadPoolWorkQueue.Dispatch()
bei System.Threading._ThreadPoolWaitCallback.PerformWaitCallback()

Я проверил значение свойства SynchronizationContext.Current. во время создания объекта System.Progress, но он никогда не равен нулю. Объект SynchronizationContext, возвращаемый свойством, имеет следующие типы:

  • Хороший случай (т.е. перед открытием диалогового окна WinForms): объект представляет собой System.Windows.Forms.WindowsFormsSynchronizationContext
  • Плохой случай (т.е. после открытия диалогового окна WinForms): объект представляет собой System.Threading.SynchronizationContext

К сожалению, у меня не так много опыта работы с WinForms и вообще с SynchronizationContext, поэтому я совершенно не понимаю, что здесь происходит.

Почему открытие диалогового окна WinForms изменяет значение SynchronizationContext.Current? Почему это влияет на поведение System.Progress? Есть ли способ «исправить» проблему, кроме написания собственной замены System.Progress?

EDIT: я мог бы добавить, что исполняемый файл представляет собой приложение MFC по своей сути, проект .exe скомпилирован с помощью /CLR, а код C#, на который я смотрю, вызывается через C++/CLI. Код C# скомпилирован для (и работает под) .NET framework 4.5.1. Сложная настройка связана с тем, что приложение является устаревшим зверем с современными взглядами :-), но пока это сработало для нас очень хорошо.


person herzbube    schedule 18.11.2016    source источник
comment
Вы показываете диалоговую форму в неосновном потоке пользовательского интерфейса?   -  person Ivan Stoev    schedule 18.11.2016
comment
@IvanStoev Нет, я показываю диалоговое окно WinForms в основном потоке пользовательского интерфейса. Я также показываю диалоговое окно прогресса WPF в основном потоке пользовательского интерфейса.   -  person herzbube    schedule 18.11.2016
comment
Похоже, вы правы. WindowsFormsSynchronizationContext автоматически удаляется, когда счетчик внутреннего цикла сообщений становится равным 0. Обычно этого не происходит в приложении WF, потому что всегда работает 1 основной цикл (Application.Run), но с архитектурой вашего приложения... Понятия не имею, даже если вы обманете каким-то образом, для правильной работы действительно требуется поддержка цикла сообщений.   -  person Ivan Stoev    schedule 18.11.2016
comment
Так что не может быть и речи о создании нового экземпляра System.Progress каждый раз, когда вы начинаете фоновую работу? Используете ли вы один и тот же экземпляр System.Progress на протяжении всей жизни приложения?   -  person Felix Castor    schedule 18.11.2016
comment
@FelixCastor Нет, я не использую один и тот же экземпляр System.Progress снова и снова, новый экземпляр System.Progress создается каждый раз непосредственно перед созданием фоновой задачи. Затем новый экземпляр подбирает то, что SynchronizationContext является текущим.   -  person herzbube    schedule 18.11.2016
comment
Почему бы тогда не попробовать наоборот? Сохраняйте тот же экземпляр.   -  person Felix Castor    schedule 18.11.2016
comment
@FelixCastor Возможно, у меня возник соблазн сделать это, но тем временем у меня есть еще один случай, не связанный с System.Progress, когда код, который должен выполняться в основном потоке, выполняется в каком-то другом потоке. Теперь я считаю, что сброс на SynchronizationContext является основной причиной, которую я должен устранить, иначе в неожиданных углах всплывут новые злые дела.   -  person herzbube    schedule 18.11.2016
comment
@IvanStoev Я экспериментировал с сохранением текущего WindowsFormsSynchronizationContext , а затем восстановлением его после удаления (с SynchronizationContext.SetSynchronizationContext()). Это похоже работает нормально, но, как я сказал в своем вопросе, у меня нет опыта работы с контекстами синхронизации. Могли бы вы сказать, что восстановление предыдущего контекста — это то, что можно сделать, не создавая дополнительных проблем?   -  person herzbube    schedule 18.11.2016
comment
Я не знаю. Должна быть причина, по которой MS помещает этот код. С другой стороны, если это работает для вас... :)   -  person Ivan Stoev    schedule 18.11.2016
comment
Это еще не все, хрустальный шар говорит, что вызов ShowDialog() выполняется в рабочем потоке. Не делай этого. Назначение WindowsFormsSynchronizationContext.AutoInstall = false; был бы другой путь.   -  person Hans Passant    schedule 19.11.2016
comment
@HansPassant Ваш хрустальный шар сегодня облачен - вызов ShowDialog() выполняется в основном потоке пользовательского интерфейса. Например, я вижу WinMainCRTStartup(), а также AfxWinMain() в нижней части стека вызовов.   -  person herzbube    schedule 21.11.2016
comment
Хм, никак не мог догадаться, что на самом деле MFC запускает цикл диспетчера. Имея не менее 3 библиотек классов графического интерфейса, которые критически зависят от наличия правильного диспетчера, выполняющего работу, что может пойти не так? Да, это.   -  person Hans Passant    schedule 21.11.2016


Ответы (1)


Интересная находка. По умолчанию WindowsFormSynhronizationContext автоматически устанавливается внутри любого конструктора класса Control (включая Form), а также в первом цикле обработки сообщений и удаляется после последнего цикла обработки сообщений. Обычно такое поведение при удалении не наблюдается, потому что приложения WinForms обычно живут внутри вызова Application.Run.

Но не в вашем случае. Проблема может быть легко воспроизведена с помощью следующего простого приложения WF:

using System;
using System.Diagnostics;
using System.Threading;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    static class Program
    {
        [STAThread]
        static void Main()
        {
            var form = new Form();
            Trace.WriteLine(SynchronizationContext.Current?.GetType().ToString() ?? "null");
            form.ShowDialog();
            Trace.WriteLine(SynchronizationContext.Current?.GetType().ToString() ?? "null");
        }
    }
}

Результат:

System.Windows.Forms.WindowsFormsSynchronizationContext
System.Threading.SynchronizationContext

В качестве обходного пути я предлагаю вам установить основной поток пользовательского интерфейса SynchronizationContext вручную в начале вашего приложения, а затем включить AutoInstall off, что предотвратит такое поведение при удалении (но может вызвать проблемы, если какая-то другая часть приложения заменит основной поток SynchronizationContext):

SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());
WindowsFormsSynchronizationContext.AutoInstall = false;
person Ivan Stoev    schedule 18.11.2016