为什么在UI线程上输入锁定会触发OnPaint事件?

我遇到了一些我根本不理解的东西。 在我的应用程序中,我有几个线程都将项添加(和删除)到共享集合(使用共享锁)。 UI线程使用计时器,并在每个tick上使用集合来更新其UI。

由于我们不希望UI线程长时间保持锁定并阻止其他线程,我们这样做的方式是,首先我们获取锁,我们复制集合,然后释放锁,然后在我们的副本上工作。 代码如下所示:

public void GUIRefresh() { ///... List 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)后面是NativeWindow.Callback,它导致OnPaint。

  • 怎么可能? UI线程是否被劫持以检查其消息泵? 那有意义吗? 或者这里有什么别的吗?

  • 有没有办法避免它? 我不希望从锁内调用OnPaint。

谢谢。

GUI应用程序的主要线程是STA线程,Single Threaded Apartment。 注意程序的Main()方法的[STAThread]属性。 STA是一个COM术语,它为从根本上是线程不安全的组件提供了一个好客的家,允许从工作线程调用它们。 COM在.NET应用程序中仍然非常活跃。 拖放,剪贴板,OpenFileDialog等shell对话框和WebBrowser等常用控件都是单线程COM对象。 STA是UI线程的硬性要求。

STA线程的行为契约是它必须泵送消息循环并且不允许阻塞。 阻塞很可能导致死锁,因为它不允许对这些单元线程COM组件进行编组。 您使用lock语句阻止该线程。

CLR非常了解这一要求,并对此做了些什么。 阻止Monitor.Enter(),WaitHandle.WaitOne / Any()或Thread.Join()之类的调用会引发一个消息循环。 执行该操作的本机Windows API类型是MsgWaitForMultipleObjects()。 该消息循环调度Windows消息以使STA保持活动状态,包括绘制消息。 这当然会导致重入问题,Paint应该不是问题。

这篇Chris Brumme博客文章中提供了很好的背景信息。

也许这一切都响了,你可能会不由自主地注意到这听起来很像一个叫做Application.DoEvents()的app。 可能是解决UI冻结问题的最可怕的方法。 对于幕后发生的事情,这是一个非常准确的心智模型,DoEvents()也会为消息循环提供支持。 唯一的区别是CLR的等价物对它允许分派的消息有一点选择性,它会对它们进行过滤。 与调度一切的DoEvents()不同。 不幸的是,Brumme的post和SSCLI20源都没有足够的详细信息来确切知道什么是调度,实际的CLR函数在源代码中不可用,而且太大而无法反编译。 但显然你可以看到它不会过滤WM_PAINT。 它将过滤真正的麻烦制造者,输入事件通知,例如允许用户关闭窗口或单击按钮的类型。

function,而不是错误。 通过消除阻塞并依赖编组回调来避免重新引发头痛。 BackgroundWorker.RunWorkerCompleted是一个经典的例子。

好问题!

.NET中的所有等待都是“可警告的”。 这意味着如果等待块,Windows可以在等待堆栈的顶部运行“异步过程调用”。 这可以包括处理一些Windows消息。 我没有特别试过WM_PAINT,但根据你的观察,我猜它包括在内。

一些MSDN链接:

等待function

异步过程调用

Joe Duffy的书“Windows上的并发编程”也涵盖了这一点。

我在等待句柄上遇到阻塞问题时发现了这个问题。 对此的回答让我暗示下一步实施:

  public static class NativeMethods { [DllImport("kernel32.dll", SetLastError = true)] internal static extern UInt32 WaitForSingleObject(SafeWaitHandle hHandle, UInt32 dwMilliseconds); } public static class WaitHandleExtensions { const UInt32 INFINITE = 0xFFFFFFFF; const UInt32 WAIT_ABANDONED = 0x00000080; const UInt32 WAIT_OBJECT_0 = 0x00000000; const UInt32 WAIT_TIMEOUT = 0x00000102; const UInt32 WAIT_FAILED = INFINITE; ///  /// Waits preventing an I/O completion routine or an APC for execution by the waiting thread (unlike default `alertable` .NET wait). Eg prevents STA message pump in background. ///  ///  ///  /// Why did entering a lock on a UI thread trigger an OnPaint event? ///  public static bool WaitOneNonAlertable(this WaitHandle current, int millisecondsTimeout) { if (millisecondsTimeout < -1) throw new ArgumentOutOfRangeException("millisecondsTimeout", millisecondsTimeout, "Bad wait timeout"); uint ret = NativeMethods.WaitForSingleObject(current.SafeWaitHandle, (UInt32)millisecondsTimeout); switch (ret) { case WAIT_OBJECT_0: return true; case WAIT_TIMEOUT: return false; case WAIT_ABANDONED: throw new AbandonedMutexException(); case WAIT_FAILED: throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error()); default: return false; } } }