gpt4 book ai didi

c# - 通过右键单击任务栏可以防止WinForms中的死锁

转载 作者:行者123 更新时间:2023-12-02 00:12:30 26 4
gpt4 key购买 nike

我的Windows C#/ .NET应用程序遇到了一个奇怪的问题。实际上,这是一个GUI应用程序,我的工作是封装在程序集中的包含的网络组件。我不知道main / 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中会发生很多)
“启动”方法尝试获取组件的锁定(在GUI线程上),块
GUI线程现在已挂起(等待启动处理程序完成;启动处理程序等待锁;该锁由将GUI更新调用编组为GUI线程并等待更新调用完成的工作线程持有;该GUI更新调用从以下位置进行编组:工作线程正在等待在其前面剪切的启动处理程序完成; ...)
如果现在右键单击任务栏,我猜想任务栏管理器(某种程度上)会启动“子事件循环”(就像模式对话框启动了自己的“子事件循环”一样,有关详细信息,请参阅Raymond Chen的博客)并处理应用程序的排队事件
右键单击触发的额外事件循环现在可以处理从工作线程编组的GUI更新。这解除了工作线程的阻塞;依次释放锁;这反过来会解除阻止应用程序的GUI线程,因此它可以完成对开始按钮单击的处理(因为它现在可以获取锁定)


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

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

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

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


但最重要的是,我将审查谁是您组件的目标客户。从方法名称和描述中,我怀疑GUI是唯一的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);
}
}
}
}


如您所见,代码分为三部分-首先,该组件使用计时器每500毫秒调用一次通知方法;其次,带有标签和开始/停止按钮的简单表格;最后是运行偶数循环的主函数。

您可以通过单击开始按钮,然后在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线程将永远不会拥有该锁,该锁将仅在(每个调用可能不同)线程池工作线程的上下文中获取。

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

[更新3]

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

        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;

关于c# - 通过右键单击任务栏可以防止WinForms中的死锁,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/1918606/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com