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

本文关键字:OnPaint 事件 UI 线程 输入 为什么 | 更新日期: 2023-09-27 18:17:05

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

  • 这怎么可能?UI线程是否被劫持以检查其消息泵?明白了吗?还是有别的原因?

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

谢谢。

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

一个GUI应用程序的主线程是一个STA线程,单线程公寓。注意Main()方法中的[STAThread]属性。STA是一个COM术语,它为基本上线程不安全的组件提供了一个友好的家,允许从工作线程调用它们。COM在。net应用中仍然非常活跃。拖放、剪贴板、shell对话框(如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。它将过滤真正的麻烦制造者,输入事件通知,如允许用户关闭窗口或单击按钮的那种。

特性,而不是bug。通过移除阻塞和依赖封送回调来避免重入问题。BackgroundWorker。RunWorkerCompleted是一个经典的例子

好问题!

. net中的所有等待都是"alertable "的。这意味着如果一个等待阻塞,Windows可以在等待堆栈上运行"异步过程调用"。这可以包括处理一些windows消息。我没有特别尝试过WM_PAINT,但从你的观察来看,我想它包括在内。

部分MSDN链接:

等待函数

异步过程调用

Joe Duffy的书《Concurrent Programming on 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;
    /// <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;
        }
    }
}