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

tza*_*chs 9 c# multithreading winforms

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

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

public void GUIRefresh()
{
    ///...
    List<Item> tmpList;
    lock (Locker)
    {
         tmpList = SharedList.ToList();
    }
    // Update the datagrid using the tmp list.
}
Run Code Online (Sandbox Code Playgroud)

虽然它工作正常,但我们注意到应用程序有时会出现速度减慢,当我们设法捕获堆栈跟踪时,我们看到了:

....
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)
....
Run Code Online (Sandbox Code Playgroud)

请注意,进入锁定(Monitor.Enter)后面是NativeWindow.Callback,它会导致OnPaint.

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

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

谢谢.

Han*_*ant 16

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的帖子和SSCLI20源都没有足够的详细信息来确切知道发送的是什么,实际的CLR功能在源代码中不可用而且太大而无法反编译.但显然你可以看到它没有过滤WM_PAINT.它将过滤真正的麻烦制造者,输入事件通知,例如允许用户关闭窗口或单击按钮的类型.

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


Nic*_*ler 5

好问题!

.NET 中的所有等待都是“可警报的”。这意味着如果等待阻塞,Windows 可以在等待堆栈的顶部运行“异步过程调用”。这可以包括处理一些 Windows 消息。我还没有专门尝试过 WM_PAINT,但从你的观察来看,我猜它是包含在内的。

一些 MSDN 链接:

等待函数

异步过程调用

Joe Duffy 的“Concurrent Programming on Windows”一书也涵盖了这一点。