至于为什么右键单击“解锁”应用程序,我对导致这种行为的事件的“有根据的猜测”如下所示:
-
(创建组件时)GUI向状态通知事件注册了订阅服务器
-
组件获得锁(在工作线程中,
不
-
-
在更新过程中,单击“开始”按钮
-
Win32向GUI线程发送单击消息,并尝试同步处理该消息
-
“开始”按钮的处理程序被调用,然后它调用组件上的“开始”方法(在GUI线程上)
-
其余的GUI更新在状态更新中(实际上在Win32中经常发生这种情况)
-
“Start”方法尝试获取组件的锁(在GUI线程上),块
-
-
如果现在右键单击任务栏,我的
猜测
任务栏管理器(以某种方式)启动一个“子事件循环”(很像模态对话框启动自己的“子事件循环”,详细信息请参见Raymond Chen的博客)并处理应用程序的排队事件
-
右键单击触发的额外事件循环现在可以处理从工作线程封送的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秒内单击“停止”按钮来死锁应用程序。然而,当我右键单击任务栏时,应用程序并没有“解冻”,叹气。
当我进入死锁的应用程序时,切换到工作者(计时器)线程时,我会看到:
这就是我切换到主线程时看到的:
如果您能尝试编译并运行此示例,我将不胜感激;如果它对您和我都适用,您可以尝试更新代码,使其与应用程序中的代码更加相似,或许我们可以重现您的确切问题。一旦我们在这样的测试应用程序中复制了它,重构它以消除问题应该不是问题(我们将隔离问题的本质)。
[
更新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;