繁体   English   中英

通过右键单击任务栏阻止WinForms中的死锁

[英]Deadlock in WinForms that is prevented by right click on the taskbar

我的Windows C#/ .NET应用程序遇到了一个奇怪的问题。 实际上它是一个GUI应用程序,我的工作是包含的网络组件,封装在一个程序集中。 我不知道主/ GUI应用程序的代码 ,我可以联系它的开发人员。

现在,应用程序的UI具有“开始”和“停止”网络引擎的按钮。 两个按钮都有效。 为了使我的组件线程安全,我使用三种方法锁定。 我不希望客户端能够在Start()完成之前调用Stop()。 另外还有一个轮询计时器。

我试着向你展示尽可能少的线条并简化问题:

private Timer actionTimer = new Timer(new
                TimerCallback(actionTimer_TimerCallback),
                null, Timeout.Infinite, Timeout.Infinite);

public void Start()
{
 lock (driverLock)
 {
  active = true;
  // Trigger the first timer event in 500ms
  actionTimer.Change(500, Timeout.Infinite);
 }
}

private void actionTimer_TimerCallback(object state)
{
 lock (driverLock)
 {
  if (!active) return;
  log.Debug("Before event");
  StatusEvent(this, new StatusEventArgs()); // it hangs here
  log.Debug("After event");
  // Now restart timer
  actionTimer.Change(500, Timeout.Infinite);
 }
}

public void Stop()
{
 lock (driverLock)
 {
  active = false;
 }
}

这是如何重现我的问题。 正如我所说,启动和停止按钮都可以工作,但是如果你按下Start(),并且在执行TimerCallback期间按下Stop() ,这会阻止TimerCallback返回。 它完全挂在相同的位置,即StatusEvent。 所以锁永远不会被释放,GUI也会挂起,因为调用Stop()方法无法继续。

现在我观察到以下情况:如果应用程序因为“死锁”而挂起,我用鼠标右键单击任务栏中的应用程序,它会继续。 它只是按预期工作。 有人对此有解释或更好的解决方案吗?

顺便说一句,我也尝试使用InvokeIfRequired,因为我不知道GUI应用程序的内部。 如果我的StatusEvent会改变GUI中的某些内容,这是必要的。 由于我没有参考GUI控件,我使用(假设只有一个目标):

Delegate firstTarget = StatusEvent.GetInocationList()[0];
ISynchronizeInvoke syncInvoke = firstTarget.Target as ISynchronizeInvoke;
if (syncInvoke.InvokeRequired)
{
  syncInvoke.Invoke(firstTarget, new object[] { this, new StatusEventArgs() });
}
else
{
  firstTarget.Method.Invoke(firstTarget.Target, new object[] { this, new StatusEventArgs() });
}

这种方法并没有改变这个问题。 我想这是因为我在主应用程序的事件处理程序上调用,而不是在GUI控件上。 那么主应用程序负责调用? 但无论如何,AFAIK虽然不需要使用Invoke,但不会导致像这样的死锁,但(希望)会出现异常。

至于为什么右键单击“解锁”您的应用程序,我对导致此行为的事件的“有根据的猜测”如下:

  1. (创建组件时)GUI向订户注册了状态通知事件
  2. 您的组件获取锁定(在工作线程中, 而不是 GUI线程),然后触发状态通知事件
  3. 调用状态通知事件的GUI回调并开始更新GUI; 更新导致事件被发送到事件循环
  4. 在进行更新时,单击“开始”按钮
  5. Win32向GUI线程发送单击消息并尝试同步处理它
  6. 调用“开始”按钮的处理程序,然后在组件上调用“启动”方法(在GUI线程上)
  7. 请注意,状态更新尚未完成; 启动按钮处理程序“切断”状态更新中剩余的GUI更新(这实际上在Win32中发生了很多)
  8. “Start”方法尝试获取组件的锁定(在GUI线程上),块
  9. GUI线程现在挂起(等待启动处理程序完成;启动处理程序等待锁定;锁定由工作线程保持,该线程编组GUI更新调用GUI线程并等待更新调用完成; GUI更新调用编组自工作线程正在等待在它前面切割的启动处理程序完成; ...)
  10. 如果您现在右键单击任务栏,我的猜测是任务栏管理器(不知何故)启动“子事件循环”(很像模态对话框启动他们自己的“子事件循环”,详情请参阅Raymond Chen的博客)并处理应用程序的排队事件
  11. 右键单击触发的额外事件循环现在可以处理从工作线程编组的GUI更新; 这解除了工作线程的阻塞; 这反过来释放锁; 这反过来取消阻止应用程序的GUI线程,以便它可以完成处理开始按钮单击(因为它现在可以获取锁定)

您可以通过使应用程序“咬”,然后进入调试器并查看组件的工作线程的堆栈跟踪来测试此理论。 它应该在某些过渡到GUI线程时被阻止。 应该在lock语句中阻止GUI线程本身,但是在堆栈中你应该能够看到一些“在线前切”调用...

我认为能够跟踪此问题的第一个建议是打开标志Control.CheckForIllegalCrossThreadCalls = true;

接下来,我建议在锁之外触发通知事件。 我通常做的是收集锁内事件所需的信息,然后释放锁并使用我收集的信息来触发事件。 一些事情:

string status;
lock (driverLock) {
    if (!active) { return; }
    status = ...
    actionTimer.Change(500, Timeout.Infinite);
}
StatusEvent(this, new StatusEventArgs(status));

但最重要的是,我会审查谁是您组件的目标客户。 从方法名称和您的描述我怀疑GUI是唯一的(它告诉您何时开始和停止;当您的状态发生变化时告诉它)​​。 在这种情况下,您应该使用锁。 启动和停止方法可以简单地设置和重置手动重置事件,以指示您的组件是否处于活动状态(真正的信号量)。

[ 更新 ]

在尝试重现您的场景时,我编写了以下简单程序。 您应该能够复制代码,编译并运行它没有问题(我将其构建为启动表单的控制台应用程序:-))

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

using Timer=System.Threading.Timer;

namespace LockTest
{
    public static class Program
    {
        // Used by component's notification event
        private sealed class MyEventArgs : EventArgs
        {
            public string NotificationText { get; set; }
        }

        // Simple component implementation; fires notification event 500 msecs after previous notification event finished
        private sealed class MyComponent
        {
            public MyComponent()
            {
                this._timer = new Timer(this.Notify, null, -1, -1); // not started yet
            }

            public void Start()
            {
                lock (this._lock)
                {
                    if (!this._active)
                    {
                        this._active = true;
                        this._timer.Change(TimeSpan.FromMilliseconds(500d), TimeSpan.FromMilliseconds(-1d));
                    }
                }
            }

            public void Stop()
            {
                lock (this._lock)
                {
                    this._active = false;
                }
            }

            public event EventHandler<MyEventArgs> Notification;

            private void Notify(object ignore) // this will be invoked invoked in the context of a threadpool worker thread
            {
                lock (this._lock)
                {
                    if (!this._active) { return; }
                    var notification = this.Notification; // make a local copy
                    if (notification != null)
                    {
                        notification(this, new MyEventArgs { NotificationText = "Now is " + DateTime.Now.ToString("o") });
                    }
                    this._timer.Change(TimeSpan.FromMilliseconds(500d), TimeSpan.FromMilliseconds(-1d)); // rinse and repeat
                }
            }

            private bool _active;
            private readonly object _lock = new object();
            private readonly Timer _timer;
        }

        // Simple form to excercise our component
        private sealed class MyForm : Form
        {
            public MyForm()
            {
                this.Text = "UI Lock Demo";
                this.AutoSize = true;
                this.AutoSizeMode = AutoSizeMode.GrowAndShrink;

                var container = new FlowLayoutPanel { FlowDirection = FlowDirection.TopDown, Dock = DockStyle.Fill, AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink };
                this.Controls.Add(container);
                this._status = new Label { Width = 300, Text = "Ready, press Start" };
                container.Controls.Add(this._status);
                this._component.Notification += this.UpdateStatus;
                var button = new Button { Text = "Start" };
                button.Click += (sender, args) => this._component.Start();
                container.Controls.Add(button);
                button = new Button { Text = "Stop" };
                button.Click += (sender, args) => this._component.Stop();
                container.Controls.Add(button);
            }

            private void UpdateStatus(object sender, MyEventArgs args)
            {
                if (this.InvokeRequired)
                {
                    Thread.Sleep(2000);
                    this.Invoke(new EventHandler<MyEventArgs>(this.UpdateStatus), sender, args);
                }
                else
                {
                    this._status.Text = args.NotificationText;
                }
            }

            private readonly Label _status;
            private readonly MyComponent _component = new MyComponent();
        }

        // Program entry point, runs event loop for the form that excercises out component
        public static void Main(string[] args)
        {
            Control.CheckForIllegalCrossThreadCalls = true;
            Application.EnableVisualStyles();
            using (var form = new MyForm())
            {
                Application.Run(form);
            }
        }
    }
}

如您所见,代码有3个部分 - 首先,每500毫秒使用timer调用通知方法的组件; 第二,带标签和开始/停止按钮的简单表格; 最后运行偶数循环的主要功能。

您可以通过单击开始按钮使应用程序死锁,然后在2秒内单击停止按钮。 但是,当我右键单击任务栏时,应用程序不会“解冻”,叹息。

当我进入死锁应用程序时,这是我在切换到worker(计时器)线程时看到的:

工人线程

这是我切换到主线程时看到的内容:

主线程

如果您可以尝试编译并运行此示例,我将不胜感激; 如果它对我来说和我一样,你可以尝试更新代码,使其更类似于你在应用程序中的代码,也许我们可以重现你的确切问题。 一旦我们在这样的测试应用程序中重现它,重构它以使问题消失应该不是问题(我们将隔离问题的本质)。

[ 更新2 ]

我想我们同意我们不能轻易地使用我提供的示例重现您的行为。 我仍然非常确定您的方案中的死锁是通过右键单击引入的额外偶数循环来破坏的,并且此事件循环处理来自通知回调的待处理消息。 但是,如何实现这一点超出了我的范围。

那说我想提出以下建议。 您可以在应用程序中尝试这些更改,并告诉我他们是否解决了死锁问题? 基本上,您将所有组件代码移动到工作线程(即除了代理工作线程的代码之外,任何与组件无关的内容都将在GUI线程上运行:-))...

        public void Start()
        {
            ThreadPool.QueueUserWorkItem(delegate // added
            {
                lock (this._lock)
                {
                    if (!this._active)
                    {
                        this._active = true;
                        this._timer.Change(TimeSpan.FromMilliseconds(500d), TimeSpan.FromMilliseconds(-1d));
                    }
                }
            });
        }

        public void Stop()
        {
            ThreadPool.QueueUserWorkItem(delegate // added
            {
                lock (this._lock)
                {
                    this._active = false;
                }
            });
        }

我将Start和Stop方法的主体移动到线程池工作线程中(就像你的计时器在线程池工作者的上下文中定期调用你的回调一样)。 这意味着GUI线程永远不会拥有锁,只会在(可能每个调用可能不同)线程池工作线程的上下文中获取锁。

请注意,通过上面的更改,我的示例程序不再死锁(即使使用“Invoke”而不是“BeginInvoke”)。

[ 更新3 ]

根据您的评论,排队Start方法是不可接受的,因为它需要指示组件是否能够启动。 在这种情况下,我建议不同地处理“活跃”标志。 您将切换到“int”(0停止,1运行)并使用“Interlocked”静态方法来操作它(我假设您的组件具有更多暴露的状态 - 您将保护访问除“活动”标志之外的任何其他标记锁):

        public bool Start()
        {
            if (0 == Interlocked.CompareExchange(ref this._active, 0, 0)) // will evaluate to true if we're not started; this is a variation on the double-checked locking pattern, without the problems associated with lack of memory barriers (see http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html)
            {
                lock (this._lock) // serialize all Start calls that are invoked on an un-started component from different threads
                {
                    if (this._active == 0) // make sure only the first Start call gets through to actual start, 2nd part of double-checked locking pattern
                    {
                        // run component startup

                        this._timer.Change(TimeSpan.FromMilliseconds(500d), TimeSpan.FromMilliseconds(-1d));
                        Interlocked.Exchange(ref this._active, 1); // now mark the component as successfully started
                    }
                }
            }
            return true;
        }

        public void Stop()
        {
            Interlocked.Exchange(ref this._active, 0);
        }

        private void Notify(object ignore) // this will be invoked invoked in the context of a threadpool worker thread
        {
            if (0 != Interlocked.CompareExchange(ref this._active, 0, 0)) // only handle the timer event in started components (notice the pattern is the same as in Start method except for the return value comparison)
            {
                lock (this._lock) // protect internal state
                {
                    if (this._active != 0)
                    {
                        var notification = this.Notification; // make a local copy
                        if (notification != null)
                        {
                            notification(this, new MyEventArgs { NotificationText = "Now is " + DateTime.Now.ToString("o") });
                        }
                        this._timer.Change(TimeSpan.FromMilliseconds(500d), TimeSpan.FromMilliseconds(-1d)); // rinse and repeat
                    }
                }
            }
        }

        private int _active;

在查看代码时会想到几件事。 第一件事是你在激活状态事件之前没有检查空委托。 如果没有绑定到事件的侦听器,那么这将导致异常,如果没有捕获或处理,可能会在线程代码中引起奇怪的问题。

所以我首先要做的是:

if(StatusEvent != null)
{
  StatusEvent(this, new StatusEventArgs());
}

想到的另一件事是,你的锁可能会以某种方式使你失败。 你用什么类型的物品来锁? 最简单的方法就是使用简单的“对象”,但是你必须确保你没有使用一个值类型(例如int,float等)来装箱锁定,因此从来没有真正建立锁定语句将框并创建一个新的对象实例。 您还应该记住,锁只会保留“其他”线程。 如果在同一个线程上调用,那么它将通过lock语句。

如果您没有GUI的源(您可能应该这样做),您可以使用Reflector来反汇编它。 甚至还有一个生成源文件的插件,因此您可以在VS IDE中运行应用程序并设置断点。

无法访问GUI源会使这更难,但这里的一般提示...... WinForm GUI不是托管代码,并且与.NET线程不能很好地混合。 建议的解决方案是使用BackgroundWorker生成独立于WinForm的线程。 一旦你在BackgroundWorker启动的线程中运行,你就可以使用纯托管代码了,你可以使用.NET的定时器和线程来完成任何事情。 限制是您必须使用BackgroundWorker的事件将信息传递回GUI,而BackgroundWorker启动的线程无法访问Winform控件。

此外,在“开始”任务运行时,您可以禁用“停止”按钮,反之亦然。 但是BackgroundWorker仍然是要走的路; 这样,后台线程运行时WinForm不会挂起。

是的,这是一个典型的死锁场景。 StatusEvent无法继续,因为它需要UI线程来更新控件。 然而,UI线程卡住了,试图获取driverLock。 由调用StatusEvent的代码持有。 两个线程都不能继续。

打破锁定的两种方法:

  • StatusEvent代码可能不一定需要同步运行。 使用BeginInvoke而不是Invoke。
  • UI线程可能不一定需要等待线程停止。 您的线程可以稍后通知它。

您的片段中没有足够的上下文来决定哪个更好。

请注意,您也可能在计时器上有可能的竞争,它在您的代码段中不可见。 但是在定时器停止后回调可能会运行一微秒。 通过使用真正的线程而不是计时器回调来避免这种头痛。 它可以通过在ManualResetEvent上调用WaitOne()来定期执行操作,并传递超时值。 那个ManualResetEvent很好地表示线程停止。

这里有一个疯狂的猜测:状态消息是否会导致其他应用程序调用您的Stop任务?

我会在所有三种方法的开头放置调试内容,看看你是否在自己陷入僵局。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM