SystemEvents.SessionSwitch 导致我的 Windows Forms 应用程序冻结

SystemEvents.SessionSwitch causing my Windows Forms Application to freeze

我在 .NET 4.5 上有一个 C# Windows Forms 应用程序。

此应用程序连接到 USB 设备。

我想同时支持多个会话

为此,我需要在会话锁定时断开与该设备的连接,以允许新会话连接到它。

我使用 SystemEvents.SessionSwitchEventArgs.Reason 来检测此类事件: - SessionSwitchReason.ConsoleDisconnect 在会话切换时 - SessionSwitchReason.ConsoleConnect 会话切换后解锁

这个事件似乎是完美的解决方案,但有时在随机时间(在多次锁定或解锁之后),事件没有被触发并且 UI 冻结。 值得注意的是,当应用程序在调试器中 运行 时不会发生这种情况。

我从日志中知道一些其他后台线程仍在正常工作,但 UI 冻结并且没有调用事件的订阅函数。

我的代码示例:

Program.cs:

using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace MyProgram
{
    static class Program
    {
        private static Mutex mutex = null;
    [STAThread]
    static void Main()
    {
        const string appName = "MyProgram";
        bool createdNew;

        mutex = new Mutex(true, appName, out createdNew);

        if (!createdNew)
        {
            //app is already running! Exiting the application  
            return;
        }

        Application.EnableVisualStyles();

        //This was one attempt to solve the UI deadlock Microsoft.Win32.SystemEvents.UserPreferenceChanged += delegate { };

        Application.SetCompatibleTextRenderingDefault(false);
        MyProgramEngine MyProgramEngine = new MyProgram.MyProgramEngine();
        Application.Run(MyProgramEngine.getForm());
    }
}

}

我的程序引擎:

using log4net;
using log4net.Config;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Timers;
using WindowsInput;
using System.Windows.Forms;
using Microsoft.Win32;


namespace MyProgram
{
    class MyProgramEngine
    {
        private MainForm mainForm;
    public MyProgramEngine()
    {
        XmlConfigurator.Configure();
        Utility.logger.Info(string.Format("MyProgram Started. Version: {0}", Application.ProductVersion));
        SystemEvents.SessionSwitch += new SessionSwitchEventHandler(SystemEvents_SessionSwitch);
        if (!GlobalSettings.getInstance().isProperlyConfigured())
        {
            WarningForm warningForm = new WarningForm("MyProgram is not properly configured. Please contact support");
            warningForm.ShowDialog();
            Application.Exit();
            Environment.Exit(0);
        }
        mainForm = new MainForm();
        initArandomBackgroundThread();
        initDeviceThread();
    }

    private void initDeviceThread()
    {
        Thread detectAndStartReader = new Thread(initDevice);
        detectAndStartReader.IsBackground = true;
        detectAndStartReader.Start();
    }

    public void initDevice()
    {
        //Connect to device
        //Start device thread
    }

    public MainForm getForm()
    {
        return mainForm;
    }


    //Handles session switching events
    internal void SystemEvents_SessionSwitch(object sender, SessionSwitchEventArgs e)
    {
        try
        {
            if (e.Reason.Equals(SessionSwitchReason.ConsoleDisconnect))
            {
                DisconnectFromDevice();
                TerminateDeviceThread();
            }
            else if (e.Reason.Equals(SessionSwitchReason.ConsoleConnect))
            {
                initDeviceThread();
            }
            else
            {
                Utility.logger.Info("The following SesseionSwitchReason has been caught: " + e.Reason + " , No action!");
            }
        }
        catch (Exception ex)
        {
            Utility.logger.Error("Something bad happened while managing session switching events", ex);
        }

    }

}

注意:我对 SessionSwitchReason.SessionUnlock 或 SessionSwitchReason.SessionLock 不感兴趣,因为我不想在同一会话上对会话锁定和解锁执行任何操作。

感谢支持!

我看到您只订阅了 SystemEvents.SessionSwitch 活动,但从未取消订阅。由于 SystemEvents.SessionSwitch 是一个 STATIC 事件,您必须非常小心地在应用程序退出之前取消订阅它。如果您不取消订阅,那么您就会为内存泄漏敞开大门,这可能会导致奇怪的故障连锁反应。请参阅文档警告:

https://msdn.microsoft.com/en-us/library/microsoft.win32.systemevents.sessionswitch(v=vs.110).aspx

Because this is a static event, you must detach your event handlers when your application is disposed, or memory leaks will result.

此外,您似乎是在 UI 主线程上调用 DisconnectFromDevice(); TerminateDeviceThread();,这可以解释一些冻结,具体取决于它实际在做什么。最好向我们展示该代码的作用以进一步评论它。

我找到了错误所在。

简而言之,

永远不要在后台工作线程上创建控件。

在我的代码中,如果我删除了 SessionSwitch 事件订阅,挂起仍然会发生。我能够将主线程上的等待追溯到 SystemSettingsChanging,这也是一个 SystemEvent,但我无法控制。

在我几乎放弃试图找出这个挂起之后,我开始逐行阅读代码,这让我发现在后台线程上创建了一个表单(弹出窗口)。

这部分代码没有引起我的注意,如上面给出的示例所示。

initArandomBackgroundThread();

要了解有关此冻结的更多信息,您可以前往 Microsoft Support 进行详细解释。

微软声称的这种冻结的低级原因

This occurs if a control is created on a thread which doesn't pump messages and the UI thread receives a WM_SETTINGCHANGE message.

Common causes are a splash screens created on a secondary UI thread or any controls created on worker threads.

修正

Applications should never leave Control objects on threads without an active message pump. If Controls cannot be created on the main UI thread, they should be created on a dedicated secondary UI thread and Disposed as soon as they are no longer needed.

调试

One way to identify which windows are created on which thread is with Spy++ in the Processes view (Spy.Processes menu). Select the hung process and expand its threads to see if there are any unexpected windows. This will find the native window if it still exists; however, the problem can occur even if the native window has been destroyed, so long as the managed Control has not yet been Disposed.