【问题标题】:ManagementObjectSearcher causes re-entrancy issues for onclick handlerManagementObjectSearcher 导致 onclick 处理程序的重入问题
【发布时间】:2015-07-06 13:48:18
【问题描述】:

我在保护一段代码时遇到了一个奇怪的问题。我的应用程序是一个托盘应用程序。我在我的类 (ApplicationContext) 中创建了一个 NotifyIcon。我已经为 NotifyIcon 对象分配了一个气球点击处理程序和一个双击处理程序。还有一个上下文菜单,但我没有显示所有代码。只有重要的部分。

 public class SysTrayApplicationContext: ApplicationContext
 {
    private NotifyIcon notifyIcon;
    private MainForm afDashBoardForm;

   public SysTrayApplicationContext()
   {
    this.notifyIcon = new NotifyIcon();
    this.notifyIcon.BalloonTipClicked += notifyIcon_BalloonTipClicked;
    this.notifyIcon.MouseDoubleClick += notifyIcon_MouseDoubleClick;
     // ... more code
   }

两个处理程序都启动或创建/显示我的表单:

        private void notifyIcon_MouseDoubleClick(object sender, MouseEventArgs e)
       {
          if (e.Button == MouseButtons.Left)
          {
              openDashboard();
          }
       }

    private void notifyIcon_BalloonTipClicked(object sender, EventArgs e)
    {
        openDashboard();
    }

    private void openDashboard()
    {
            if (dashBoardForm != null)
            {
                log.Debug("Dashboard form created already, so Activate it");
                dashBoardForm.Activate();
            }
            else
            {
                log.Debug("Dashboard form does not exist, create it");
                dashBoardForm = new MainForm();
                dashBoardForm.Show();
            }
    }

上面的代码有问题。可能不止 1 个。问题:可以显示 2 个仪表板表单,这不是我想要的。如果用户在显示气球消息时双击托盘图标会导致 openDashboard 中出现竞争条件。我可以很容易地重现这一点。因此,我在 openDashboard 代码中的代码周围添加了一个锁,令我惊讶的是,这并没有阻止显示 2 个仪表板表单。我应该无法创建 2 个 MainForms。我哪里错了?

这里是带有锁语句的更新代码:

        private void openDashboard()
        {
          lock (dashBoardFormlocker)
          {
            if (dashBoardForm != null)
            {
                log.Debug("Dashboard form created already, so Activate it");
                dashBoardForm.Activate();
            }
            else
            {
                log.Debug("Dashboard form does not exist, create it");
                dashBoardForm = new MainForm();
                dashBoardForm.Show();
            }
         }
      }

注意:锁对象已添加到类中并在构造函数中初始化。

private object dashBoardFormlocker;

更新:显示更多代码。这就是代码开始的方式:

    static void Main()
    {
        if (SingleInstance.Start())
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            XmlConfigurator.Configure();
            // For a system tray application we don't want to create 
            // a form, we instead create a new ApplicationContext.  The Run method takes
            Application.Run(new SysTrayApplicationContext());
            SingleInstance.Stop();
            SingleInstance.Dispose();
        }
    }
}

更新 2:为清晰起见提供更多代码

public partial class MainForm : Form
{

    public MainForm()
    {
        log.Trace("MainForm constructor...");
        InitializeComponent();
        // ... code not shown
        this.label_OSVersion.Text = getOSFriendlyName();

        // .. more code
    }

    private string getOSFriendlyName()
    {
        try
        {
            string result = string.Empty;
            var mgmtObj = (from x in new ManagementObjectSearcher("SELECT Caption FROM Win32_OperatingSystem").Get().OfType<ManagementObject>()
                           select x.GetPropertyValue("Caption")).FirstOrDefault();
            result = mgmtObj != null ? mgmtObj.ToString() : string.Empty;
            OperatingSystem os = Environment.OSVersion; 
            String sp = os.ServicePack ?? string.Empty;

            return !string.IsNullOrWhiteSpace(result) ? result + sp : "Unknown";
        }
        catch (System.Exception ex)
        {
            log.Error("Error trying to get the OS version", ex);
            return "Unknown";
        }
    }
}

【问题讨论】:

  • 这里面的多线程在哪里?表单(以及几乎所有标准 Windows UI 组件)共享一个线程,因此 lock 不会像您预期的那样工作。
  • 我认为气球点击会在不同的线程上运行。
  • 为什么会这样?它将WM_CLICK(或其他)发布到消息传递/窗口系统内的消息队列中。消息泵将其拾取并触发相应的事件。
  • 另外,NotifyIcon 是否被视为标准 UI 组件?此应用程序没有主窗体。表单稍后会更新。
  • 是的。它仍然适用于相同的基本消息传递/窗口系统。

标签: c# multithreading winforms


【解决方案1】:

主 UI 线程必须始终泵送消息循环以支持来自 COM 组件的通信。 因此,当您从 UI 线程执行阻塞操作(例如锁定或加入线程)时(编辑:根据 Peter Duniho 的修复进行编辑),UI 线程将进入“可提醒”状态,允许 COM 调度某些类型的消息,其中turn 可能会导致像您的场景一样的重入问题。 查看这个问题的答案 (Why did entering a lock on a UI thread trigger an OnPaint event?) 以获得更准确的解释。

查看ManagementObjectSearcher.Get的源码有一个锁(在Initialize里面),由于你是从你的form的构造函数中调用的,可能会导致第二个事件触发,而form的构造函数还没有完成.对 dashBoardFormlocker 变量的赋值仅在构造函数完成后发生,因此这可以解释为什么它在第二个条目上为空。

故事的寓意是永远不要在 UI 线程上执行阻塞操作。

【讨论】:

  • 关于 ManagementObjectSearcher 的好信息。您是说 Initialize 方法中的锁导致第一个事件处理程序被第二个事件抢占吗?我认为这不会发生在单个 UI 线程上。我肯定希望看到这两个事件都发生。我不希望看到一个事件处理程序在它完成允许另一个事件“交替式”运行之前停止。
  • 注意:我确实删除了我这边的所有锁定代码。问题并没有停止。它似乎与 ManagementObjectSearcher.Get() 完全相关。
  • “因此,当您从 UI 线程执行阻塞操作(例如锁定或加入线程)时,CLR 会故意等待传入事件”——恕我直言,这是误导。实际发生的情况是,作为线程进入“警报”状态的副作用,COM 有可能参与并分派某些消息,这反过来又会导致鼠标消息重入。这实际上更像是一个 WIndows 的事情,而不是 CLR 专门为避免死锁而做的事情(事实上,GUI 线程死锁对于错误编写的代码来说是一个非常现实且不可避免的问题)。
  • @Jim: “你是说 Initialize 方法中的锁导致第一个事件处理程序被第二个事件抢占” - 这就是所说的,这是完全合理的。只需在调试器中设置断点并在第二个鼠标事件出现时查看堆栈跟踪。您可能会在NativeWindow.Callback() 之前看到对Monitor.Enter() 的调用以及对第二个鼠标消息的后续处理。事件处理不是“交替”的,它是可重入的;即第一个事件的处理被中断,直到第二个事件的处理完成才恢复。
  • @Peter Duniho:感谢您的修复,我的这句话是基于 Hans Passant 对我链接到的问题的回答,特别是这句话:“CLR 非常清楚这一要求并做了一些事情关于它。”也许我误解了。我根据您的解释编辑了答案。
【解决方案2】:

如果没有可靠地重现问题的a good, minimal, complete code example,就不可能确定问题出在哪里。但是the guess by answerer tzachs 似乎是合理的。如果是这样,您可以通过将方法更改为如下所示来解决您的问题:

private bool _dashboardOpen;

private void openDashboard()
{
    if (_dashboardOpen)
    {
        if (dashBoardForm != null)
        {
            log.Debug("Dashboard form created already, so Activate it");
            dashBoardForm.Activate();
        }
    }
    else
    {
        log.Debug("Dashboard form does not exist, create it");
        _dashboardOpen = true;
        dashBoardForm = new MainForm();
        dashBoardForm.Show();
    }
}

这样,任何重新打开窗口的尝试都会被检测到。请注意,您仍然需要在实际激活之前检查null;您无法激活尚未真正完成创建的窗口。无论如何,随后对Show() 的调用将负责激活,因此在重入情况下忽略激活应该无关紧要。

【讨论】:

  • 这在我的应用程序的上下文中确实有效。当第二个事件发生时,由于 _dashboardOpen== true 和 dashBoardForm == null,它实际上被忽略了。这很好。
猜你喜欢
  • 1970-01-01
  • 2010-10-27
  • 2019-11-01
  • 1970-01-01
  • 2021-01-21
  • 1970-01-01
  • 2018-09-28
  • 2014-10-23
  • 1970-01-01
相关资源
最近更新 更多