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

问题描述 投票:0回答:3

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

  • 这怎么可能? UI 线程是否被劫持来检查其消息泵?这有道理吗?还是这里还有别的东西?

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

谢谢。

c# winforms multithreading
3个回答
17
投票

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()。 该消息循环调度 Windows 消息以保持 STA 处于活动状态,包括绘制消息。 当然,这可能会导致重入问题,Paint 应该不是问题。

这篇 Chris Brumme 博客文章中有关于此的很好的背景信息。

也许这一切都敲响了警钟,您可能不禁注意到这听起来很像一个应用程序调用 Application.DoEvents()。 可能是解决 UI 冻结问题的最可怕的方法。 对于幕后发生的事情来说,这是一个非常准确的心理模型,DoEvents() 还可以泵送消息循环。 唯一的区别是 CLR 的等效项对于它允许分派的消息更具选择性,它会过滤它们。 与调度所有内容的 DoEvents() 不同。 不幸的是,Brumme 的帖子和 SSCLI20 源代码都不够详细,无法准确了解正在分派的内容,执行此操作的实际 CLR 函数在源代码中不可用,而且太大而无法反编译。 但显然你可以看到它过滤WM_PAINT。 它将过滤真正的麻烦制造者,输入事件通知,例如允许用户关闭窗口或单击按钮的类型。

功能,而不是错误。 通过消除阻塞并依靠编组回调来避免重入问题。 BackgroundWorker.RunWorkerCompleted 是一个经典的例子。


5
投票

好问题!

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

一些 MSDN 链接:

等待功能

异步过程调用

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


1
投票

我在遇到等待句柄阻塞问题时发现了这个问题。这个问题的答案给了我接下来实施的提示:

 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;

    /// <summary>
    /// Waits preventing an I/O completion routine or an APC for execution by the waiting thread (unlike default `alertable`  .NET wait). E.g. prevents STA message pump in background. 
    /// </summary>
    /// <returns></returns>
    /// <seealso cref="http://stackoverflow.com/questions/8431221/why-did-entering-a-lock-on-a-ui-thread-trigger-an-onpaint-event">
    /// Why did entering a lock on a UI thread trigger an OnPaint event?
    /// </seealso>
    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;
        }
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.