Invoke((MethodInvoker)delegate ...
每当您使用lock
在你的代码中声明,那么你总是冒着诱导的风险deadlock。经典的线程错误之一。通常,您至少需要两个锁才能到达那里,并且以错误的顺序获取它们。是的,你的程序中有两个。一个你自己宣称的。还有一个你看不到的,因为它埋在使 Control.Invoke() 工作的管道内部。无法看到锁使得死锁成为难以调试的问题。
您可以推断出,Control.Invoke 内部的锁是必要的,以确保工作线程被阻塞,直到 UI 线程执行委托目标。可能还有助于推理出程序陷入僵局的原因。您启动了工作线程,它获取了lockUcLoader
lock 并开始执行其工作,同时调用 Control.Invoke。现在,您在工作人员完成之前单击按钮,它必然会阻塞。但这会使 UI 线程陷入紧张状态,并且不再能够执行 Control.Invoke 代码。因此工作线程挂起 Invoke 调用并且不会释放锁。由于工作线程无法完成,UI 线程永远挂在锁上,形成死锁。
Control.Invoke 源自 .NET 1.0,该框架的一个版本在与线程相关的代码中存在多个严重的设计错误。虽然它们的目的是提供帮助,但它们只是为程序员设置了容易陷入的死亡陷阱。 Control.Invoke 的独特之处在于它是never正确使用它。
区分 Control.Invoke 和 Control.BeginInvoke。你只曾need当需要它的返回值时调用。请注意您是如何不这么做的,使用 BeginInvoke 就足够了,并且可以立即解决死锁。您可以考虑使用 Invoke 从 UI 获取值,以便可以在工作线程中使用它。但这会引发其他主要线程问题,即线程竞争错误,工作人员不知道 UI 处于什么状态。比如说,用户可能正忙于与其交互,输入新值。你无法知道你获得了什么值,它很容易是陈旧的旧值。不可避免地会导致 UI 和正在完成的工作之间不匹配。避免这种不幸的唯一方法是prevent用户无需输入新值,只需使用 Enable = false 即可轻松完成。但现在使用 Invoke 已经没有意义了,不妨在启动线程时传递该值。
所以使用BeginInvoke已经足以解决问题了。但这不是你应该停止的地方。 Click 事件处理程序中的这些锁没有任何意义,它们所做的只是使 UI 无响应,让用户感到非常困惑。您必须做的是将这些按钮的启用属性设置为false。将它们设置回true当工人完成时。现在它不会再出错了,您不需要锁,并且用户可以获得良好的反馈。
还有一个您尚未遇到但必须解决的严重问题。 UserControl 无法控制其生命周期,当用户关闭它所在的表单时,它就会被释放。但这与工作线程执行完全不同步,即使控件已经死了,它仍然会不断调用 BeginInvoke。这将使你的程序成为炸弹,希望是在 ObjectDisposeException 上。锁无法解决的线程竞争错误。表单必须提供帮助,它必须主动阻止用户关闭它。关于此错误的一些注释this Q+A https://stackoverflow.com/a/1732361/17034.
为了完整起见,我应该提到像这样的代码可能会遇到的第三个最常见的线程错误。它没有正式名称,我称之为“firehose bug”。当工作线程过于频繁地调用 BeginInvoke,从而给 UI 线程提供太多工作要做时,就会发生这种情况。发生很容易,每秒调用它超过一千次就足够了。 UI 线程开始消耗 100% 的核心,试图跟上调用请求,但永远无法跟上。很容易看出,它停止绘制自身并响应输入,以及以较低优先级执行的任务。这需要以合乎逻辑的方式解决,每秒更新 UI 超过 25 次只会产生人眼无法观察到的模糊,因此毫无意义。