【问题标题】:System.Progress not firing events on main thread after WinForms dialog was shown显示 WinForms 对话框后,System.Progress 未在主线程上触发事件
【发布时间】:2016-11-18 16:43:28
【问题描述】:

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

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

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

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

我注意到System.Progress 的文档说:

[...] 使用ProgressChanged 事件注册的事件处理程序是通过在构造实例时捕获的SynchronizationContext 实例调用的。如果在构造时没有当前的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()

我在创建System.Progress 对象时检查了SynchronizationContext.Current 属性的值,但它永远不会为空。该属性返回的SynchronizationContext对象有以下几种类型:

  • 好案例(即在打开 WinForms 对话框之前):对象是 System.Windows.Forms.WindowsFormsSynchronizationContext
  • 坏情况(即打开 WinForms 对话框后):对象是 System.Threading.SynchronizationContext

不幸的是,我对 WinForms 没有太多经验,对 SynchronizationContext 一点经验也没有,所以我很茫然。

为什么打开 WinForms 对话框会更改 SynchronizationContext.Current 值?为什么这会对System.Progress 的行为方式产生影响?除了写我自己的System.Progress替换之外,有没有办法“解决”这个问题?

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

【问题讨论】:

  • 您是否在非主 UI 线程上显示对话框?
  • @IvanStoev 不,我在主 UI 线程上显示 WinForms 对话框。我还在主 UI 线程上显示 WPF 进度对话框。
  • 看来你是对的。当内部消息循环计数变为 0 时,WindowsFormsSynchronizationContext 会自动卸载。这通常不会在 WF 应用程序中发生,因为总是有 1 个主循环在运行(Application.Run),但是对于您的应用架构......我不知道,即使你以某种方式欺骗它,它也确实需要消息循环支持才能正常工作。
  • 那么在您开始后台工作的任何时候实例化System.Progress 的新实例是不可能的吗?您是否在应用程序的整个生命周期中使用相同的 System.Progress 实例?
  • Hmya,它不可能猜到它实际上是 MFC 运行调度程序循环。拥有不少于 3 个 gui 类库,这些类库严重依赖于正确的调度程序来完成工作,可能会出现什么问题?是的,那个。

标签: c# .net multithreading winforms synchronizationcontext


【解决方案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

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

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

【讨论】:

    猜你喜欢
    • 2012-01-04
    • 1970-01-01
    • 2012-01-13
    • 2018-08-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多