我的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,但不会导致像这样的死锁,但(希望)会出现异常.
至于为什么右键单击"解锁"您的应用程序,我对导致此行为的事件的"有根据的猜测"如下:
(创建组件时)GUI向订户注册了状态通知事件
您的组件获取锁定(在工作线程中,而不是 GUI线程),然后触发状态通知事件
调用状态通知事件的GUI回调并开始更新GUI; 更新导致事件被发送到事件循环
在进行更新时,单击"开始"按钮
Win32向GUI线程发送单击消息并尝试同步处理它
调用"开始"按钮的处理程序,然后在组件上调用"启动"方法(在GUI线程上)
请注意,状态更新尚未完成; 启动按钮处理程序"切断"状态更新中剩余的GUI更新(这实际上在Win32中发生了很多)
"Start"方法尝试获取组件的锁定(在GUI线程上),块
GUI线程现在挂起(等待启动处理程序完成;启动处理程序等待锁定;锁定由工作线程保持,该线程编组GUI更新调用GUI线程并等待更新调用完成; GUI更新调用编组自工作线程正在等待在它前面切割的启动处理程序完成; ...)
如果您现在右键单击任务栏,我的猜测是任务栏管理器(不知何故)启动"子事件循环"(很像模态对话框启动他们自己的"子事件循环",详情请参阅Raymond Chen的博客)并处理应用程序的排队事件
右键单击触发的额外事件循环现在可以处理从工作线程编组的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 EventHandlerNotification; 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 (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(计时器)线程时看到的:
工人线程http://img34.imageshack.us/img34/4286/vs1e.png
这是我切换到主线程时看到的内容:
主线http://img192.imageshack.us/img192/612/vs2.png
如果您可以尝试编译并运行此示例,我将不胜感激; 如果它和我一样工作,你可以尝试更新代码,使其更类似于你在应用程序中的代码,也许我们可以重现你的确切问题.一旦我们在这样的测试应用程序中重现它,重构它以使问题消失应该不是问题(我们将隔离问题的本质).
[ 更新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;