我遇到了一些我根本不明白的事情。 在我的应用程序中,我有几个线程都将项目添加(和删除)到共享集合(使用共享锁)。 UI 线程使用计时器,并在每次计时时使用集合更新其 UI。
由于我们不希望 UI 线程长时间持有锁并阻塞其他线程,我们的做法是首先获取锁,复制集合,释放锁定然后处理我们的副本。 代码如下所示:
public void GUIRefresh()
{
///...
List<Item> tmpList;
lock (Locker)
{
tmpList = SharedList.ToList();
}
// Update the datagrid using the tmp list.
}
虽然它工作正常,但我们注意到有时应用程序会变慢,当我们设法捕获堆栈跟踪时,我们看到了这一点:
....
at System.Windows.Forms.DataGrid.OnPaint(PaintEventArgs pe)
at MyDataGrid.OnPaint(PaintEventArgs pe)
at System.Windows.Forms.Control.PaintWithErrorHandling(PaintEventArgs e, Int16 layer, Boolean disposeEventArgs)
at System.Windows.Forms.Control.WmPaint(Message& m)
at System.Windows.Forms.Control.WndProc(Message& m)
at System.Windows.Forms.Control.ControlNativeWindow.OnMessage(Message& m)
at System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message& m)
at System.Windows.Forms.NativeWindow.Callback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)
at System.Threading.Monitor.Enter(Object obj)
at MyApplication.GuiRefresh()
at System.Windows.Forms.Timer.OnTick(EventArgs e)
at System.Windows.Forms.Timer.TimerNativeWindow.WndProc(Message& m)
at System.Windows.Forms.NativeWindow.Callback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)
at System.Windows.Forms.UnsafeNativeMethods.DispatchMessageW(MSG& msg)
at System.Windows.Forms.Application.ComponentManager.System.Windows.Forms.UnsafeNativeMethods.IMsoComponentManager.FPushMessageLoop(Int32 dwComponentID, Int32 reason, Int32 pvLoopData)
at System.Windows.Forms.Application.ThreadContext.RunMessageLoopInner(Int32 reason, ApplicationContext context)
at System.Windows.Forms.Application.ThreadContext.RunMessageLoop(Int32 reason, ApplicationContext context)
at System.Windows.Forms.Application.Run(Form mainForm)
....
请注意,进入锁定 (Monitor.Enter) 之后是导致 OnPaint 的 NativeWindow.Callback。
这怎么可能? UI 线程是否被劫持以检查其消息泵?那有意义吗?还是这里还有其他东西?
有办法避免吗?我不希望从锁内调用 OnPaint。
谢谢。
最佳答案
GUI 应用程序的主线程是 STA 线程,即单线程单元。请注意程序的 Main() 方法中的 [STAThread] 属性。 STA 是一个 COM 术语,它为基本上线程不安全的组件提供了一个好客的家,允许从工作线程调用它们。 COM 在 .NET 应用程序中仍然非常活跃。拖放、剪贴板、外壳对话框(如 OpenFileDialog)和通用控件(如 WebBrowser)都是单线程 COM 对象。 STA 是 UI 线程的硬性要求。
STA 线程的行为契约是它必须泵送消息循环并且不允许阻塞。阻塞很可能导致死锁,因为它不允许对这些单元线程 COM 组件进行编码。您正在使用 lock 语句阻塞线程。
CLR 非常清楚这一要求并为此做了一些事情。 Monitor.Enter()、WaitHandle.WaitOne/Any() 或 Thread.Join() 等阻塞调用会产生消息循环。执行此操作的原生 Windows API 是 MsgWaitForMultipleObjects().该消息循环分派(dispatch) Windows 消息以使 STA 保持事件状态,包括绘制消息。这当然会导致重入问题,Paint 应该不是问题。
在这个 Chris Brumme blog post 中有很好的背景资料信息.
也许这一切都敲响了警钟,您可能会情不自禁地注意到这听起来很像调用 Application.DoEvents() 的应用程序。可能是解决 UI 卡住问题的最可怕的方法。对于引擎盖下发生的事情,这是一个非常准确的心智模型,DoEvents() 还会启动消息循环。唯一的区别是 CLR 的等价物对它允许分派(dispatch)的消息更具选择性,它会过滤它们。与分派(dispatch)所有内容的 DoEvents() 不同。不幸的是,无论是 Brumme 的帖子还是 SSCLI20 源都不够详细,无法准确了解正在分派(dispatch)的内容,执行此操作的实际 CLR 函数在源中不可用,而且太大而无法反编译。但您可以清楚地看到它不过滤 WM_PAINT。它将过滤真正的麻烦制造者,输入事件通知,例如允许用户关闭窗口或单击按钮的那种。
功能,不是错误。通过移除阻塞并依赖编码回调来避免重入问题。 BackgroundWorker.RunWorkerCompleted 是一个典型的例子。
关于c# - 为什么在 UI 线程上锁定会触发 OnPaint 事件?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/8431221/