Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why did entering a lock on a UI thread trigger an OnPaint event?

I came across something I simply don't understand. In my application I have several threads all adding (and removing) items to a shared collection (using a shared lock). The UI thread uses a timer, and on every tick it uses the collection to update its UI.

Since we don't want the UI thread to hold on to the lock for a long time and block the other threads, the way we do it, is that first we acquire the lock, we copy the collection, we release the lock and then work on our copy. The code looks like this:

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

While it works fine, we noticed that sometimes there are slowdowns in the application, and when we managed to catch a stacktrace, we saw this:

....
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)
....

Note that entering the lock (Monitor.Enter) is followed by NativeWindow.Callback which leads to OnPaint.

  • How is that possible? Does the UI thread gets hijacked to check its message pump? Does that make sense? Or is there something else here?

  • Is there a way to avoid it? I don't want the OnPaint to be called from within the lock.

Thanks.

like image 855
tzachs Avatar asked Dec 08 '11 12:12

tzachs


3 Answers

Good question!

All waits in .NET are "alertable." This means that if a wait blocks, Windows can run "Asynchronous Procedure Calls" on top of the waiting stack. This can include processing some windows messages. I haven't tried WM_PAINT specifically, but from your observations, I guess it is included.

Some MSDN links:

Wait Functions

Asynchronous Procedure Calls

Joe Duffy's book "Concurrent Programming on Windows" also covers this.

like image 136
Nick Butler Avatar answered Oct 14 '22 15:10

Nick Butler


I found this question while having issue of blocking on wait handle. Answers to this gave me hint to implement next:

 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;
        }
    }
}
like image 40
Dzmitry Lahoda Avatar answered Oct 14 '22 15:10

Dzmitry Lahoda


The main thread of a GUI app is an STA thread, Single Threaded Apartment. Note the [STAThread] attribute on the Main() method of your program. STA is a COM term, it gives a hospitable home to components that are fundamentally thread-unsafe, allowing them to be called from a worker thread. COM is still very much alive in .NET apps. Drag and drop, the Clipboard, the shell dialogs like OpenFileDialog and common controls like WebBrowser are all single threaded COM objects. STA is a hard requirement for UI threads.

The behavioral contract for an STA thread is that it must pump a message loop and is not allowed to block. Blocking is very likely to cause deadlock since it doesn't allow the marshaling for these apartment threaded COM components to progress. You are blocking the thread with your lock statement.

The CLR is very much aware of that requirement and does something about it. Blocking calls like Monitor.Enter(), WaitHandle.WaitOne/Any() or Thread.Join() pump a message loop. The kind of native Windows API that does that is MsgWaitForMultipleObjects(). That message loop dispatches Windows messages to keep the STA alive, including paint messages. This can cause re-entrancy problems of course, Paint should not be a problem.

There's good backgrounder info on this in this Chris Brumme blog post.

Maybe this all rings a bell, you probably can't help notice that this sounds a lot like an app calling Application.DoEvents(). Probably the single-most dreaded method available to solve UI freezing problems. That's a pretty accurate mental model for what happens under the hood, DoEvents() also pumps the message loop. The only difference is that the CLR's equivalent is a bit more selective about what messages it allows to be dispatched, it filters them. Unlike DoEvents() which dispatches everything. Unfortunately neither Brumme's post nor the SSCLI20 source is sufficiently detailed to know exactly what is getting dispatched, the actual CLR function that does this is not available in source and far too large to decompile. But clearly you can see that it does not filter WM_PAINT. It will filter the real trouble-makers, input event notifications like the kind that allows the user to close a window or click a button.

Feature, not a bug. Avoid re-entrancy headaches by removing the blocking and relying on marshaled callbacks. BackgroundWorker.RunWorkerCompleted is a classic example.

like image 29
Hans Passant Avatar answered Oct 14 '22 16:10

Hans Passant