c# - 为什么在 UI 线程上锁定会触发 OnPaint 事件?

标签 c# winforms multithreading

我遇到了一些我根本不明白的事情。 在我的应用程序中,我有几个线程都将项目添加(和删除)到共享集合(使用共享锁)。 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/

相关文章:

c# - 为什么 Entity Framework 不在此处更新子对象?

c# - 当 DropDownStyle 为 DropDown 时,ComboBox Cue Banner 不是斜体

java - 同样的计算怎么会产生不同的结果

C# - DateTimePicker,检测上下点击事件

winforms - 在 Winforms 中订阅 PropertyChangedEventHandler

.net - Dapper QueryMultiple/Query/Execute 方法线程安全吗?

java - 多线程是语言(如 java)的属性还是操作系统的属性?

c# - 在 iTextSharp 中的两个表之间添加空间

c# - Observable.FromEvent 签名

c# - 从数据表获取字节数组到列表 C#