代码之家  ›  专栏  ›  技术社区  ›  Tarnschaf

WinForms中的死锁可通过右键单击任务栏来防止

  •  5
  • Tarnschaf  · 技术社区  · 16 年前

    ,我可以联系它的开发者。

    现在,应用程序的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()方法的调用无法继续。

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

    因为我没有参考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() });
    }
    

    6 回复  |  直到 13 年前
        1
  •  8
  •   Glorfindel Doug L.    7 年前

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

    1. (创建组件时)GUI向状态通知事件注册了订阅服务器
    2. 组件获得锁(在工作线程中,
    3. 在更新过程中,单击“开始”按钮
    4. Win32向GUI线程发送单击消息,并尝试同步处理该消息
    5. “开始”按钮的处理程序被调用,然后它调用组件上的“开始”方法(在GUI线程上)
    6. 其余的GUI更新在状态更新中(实际上在Win32中经常发生这种情况)
    7. “Start”方法尝试获取组件的锁(在GUI线程上),块
    8. 如果现在右键单击任务栏,我的 猜测 任务栏管理器(以某种方式)启动一个“子事件循环”(很像模态对话框启动自己的“子事件循环”,详细信息请参见Raymond Chen的博客)并处理应用程序的排队事件
    9. 右键单击触发的额外事件循环现在可以处理从工作线程封送的GUI更新;这将解锁工作线程;这反过来又会释放锁;这反过来会解锁应用程序的GUI线程,以便它可以完成对开始按钮单击的处理(因为它现在可以获得锁)

    Control.CheckForIllegalCrossThreadCalls = true; .

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

    我正在用锁。起动和起动;停止方法可以简单地设置和重置手动重置事件,以指示组件是否处于活动状态(实际上是一个信号量)。

    [ 更新

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

    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);
                }
            }
        }
    }
    

    您可以通过单击“开始”按钮,然后在2秒内单击“停止”按钮来死锁应用程序。然而,当我右键单击任务栏时,应用程序并没有“解冻”,叹气。

    当我进入死锁的应用程序时,切换到工作者(计时器)线程时,我会看到:

    Worker thread

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

    Main thread

    如果您能尝试编译并运行此示例,我将不胜感激;如果它对您和我都适用,您可以尝试更新代码,使其与应用程序中的代码更加相似,或许我们可以重现您的确切问题。一旦我们在这样的测试应用程序中复制了它,重构它以消除问题应该不是问题(我们将隔离问题的本质)。

    [ 更新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 ]

    根据您的评论,排队启动方法是不可接受的,因为它需要指示组件是否能够启动。在这种情况下,我建议以不同的方式对待“活动”标志。您可以切换到“int”(0已停止,1正在运行)并使用“联锁”静态方法对其进行操作(我假设您的组件有更多它公开的状态-您可以用锁保护对“活动”标志以外的任何内容的访问):

            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;
    
        2
  •  2
  •   Michael A. McCloskey    16 年前

    在查看代码时,会想到几件事。首先,在触发状态事件之前,您没有检查空委托。如果没有监听器绑定到事件,那么这将导致异常,如果没有捕获或处理该异常,可能会导致线程代码中出现奇怪的问题。

    所以我要做的第一件事是:

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

        3
  •  1
  •   Cory Charlton    16 年前

    如果您没有GUI的源代码(您可能应该这样做),您可以使用 Reflector 拆开它。甚至还有一个插件可以生成源文件,这样你就可以在VS IDE中运行应用程序并设置断点。

        4
  •  1
  •   Cylon Cat    16 年前

    无法访问GUI源代码使这变得更困难,但这里有一个一般提示。。。WinForm GUI不是托管代码,不能与.NET线程很好地混合。建议的解决方案是使用 BackgroundWorker

    另外,当“开始”任务正在运行时,您最好禁用“停止”按钮,反之亦然。但是一个幕后工作者仍然是一条路要走;这样,后台线程运行时WinForm不会挂起。

        5
  •  1
  •   Hans Passant    16 年前

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

    有两种方法可以解除锁定:

    • StatusEvent代码可能不一定需要同步运行。使用BeginInvoke而不是Invoke。

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

        6
  •  0
  •   Loren Pechtel    16 年前

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

    推荐文章