System.Progress在显示WinForms对话框后未在主线程上触发事件

时间:2016-11-18 16:43:29

标签: c# .net multithreading winforms synchronizationcontext

我有一个显示进度表控件的WPF对话框,以及一个后台任务(System.Threading.Tasks.Task),它提供了需要输入进度表的进度更新流。两者之间的中介是System.Progress<T>对象。

这一切都在“正常”情况下完美运作:

  • 后台任务在某个不是主线程的线程X上调用dist/dev_tool/langdetect/
  • System.IProgress.Report()对象在切换线程时发挥其内在魔力
  • System.Progress对象在主线程上触发System.Progress事件。
  • 进度表控件在主线程(拥有控件)上更新

现在,如果我打开任何 WinForms对话框,然后关闭它,然后启动我的后台任务,ProgressChanged突然激活不在主线程上的System.Progress事件,但在某些线程Y上,这不是主线程。这当然导致ProgressChanged,因为事件处理程序尝试在与拥有该控件的线程不同的线程上更新WPF进度表控件。

我注意到InvalidOperationException的文档说:

  注册System.Progress事件的

[...]事件处理程序通过构造实例时捕获的SynchronizationContext实例调用。如果在构建时没有当前SynchronizationContext,则将在ThreadPool上调用回调。

这似乎与我能观察到的相匹配,因为这是ProgressChanged在坏情况下触发事件时调用堆栈的下半部分的样子:

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属性的值,但它永远不为null。属性返回的System.Progress对象具有以下类型:

  • 好的情况(即在打开WinForms对话框之前):对象是SynchronizationContext
  • 错误的情况(即打开WinForms对话框后):对象是System.Windows.Forms.WindowsFormsSynchronizationContext

不幸的是,我对WinForms没有多少经验,而System.Threading.SynchronizationContext没有任何经验,所以我在这里发生了很多不知所措。

为什么打开WinForms对话框会更改SynchronizationContext值?为什么这会对SynchronizationContext.Current的行为产生影响?有没有办法如何“解决”问题,而不是编写我自己的System.Progress替换?

编辑:我可能会补充一点,可执行文件是核心的MFC应用程序,.exe项目是用/ CLR编译的,我正在查看的C#代码是通过C ++ /调用的CLI。 C#代码是为.NET Framework 4.5.1编译的(并在其下运行)。复杂的设置是由于应用程序是一个具有现代态度的传统野兽:-),但到目前为止,这对我们来说非常好。

1 个答案:

答案 0 :(得分:4)

有趣的发现。默认情况下,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

作为一种解决方法,我建议您在应用程序开始时手动设置主UI线程SynchronizationContext,然后关闭AutoInstall,这将阻止该卸载行为(但如果应用程序的其他部分替换主线程SynchronizationContext):

SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());
WindowsFormsSynchronizationContext.AutoInstall = false;