Why does blocking the UI thread trigger an OnPaint event?

I stumbled upon something that I just do not understand. In my application, I have several threads that add (and remove) items to the shared collection (using shared lock). A timer is used in the user interface thread, and at each tick it uses a collection to update its user interface.

Since we don’t want the UI thread to stay locked for a long time and block other threads, the way we do this is to get the lock first, we copy the collection, we release it and then work on our copy. The code is as follows:

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

While it is working fine, we noticed that sometimes there is a slowdown in the application, and when we managed to catch the stack, 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 when you enter a lock (Monitor.Enter), NativeWindow.Callback follows, which leads to OnPaint.

  • How is this possible? Is the UI thread deployed to check its message pump? Does this make sense? Or is there something else here?

  • Is there any way to avoid this? I do not want OnPaint to be called from the castle.

Thanks.

+6
source share
3 answers

The main thread of the GUI application is the STA thread, a single-threaded apartment. Note the [STAThread] attribute in the Main () method of your program. STA is the term COM, it gives a hospitable home to components that are fundamentally unsafe, which allows them to be called from a workflow. COM is still very much alive in .NET applications. Drag and drop, clipboard, shell dialogs such as OpenFileDialog, and common controls such as WebBrowser are single-threaded COM objects. STA is a tough requirement for user interface threads.

The behavioral contract for the STA flow is that it must pump the message loop and cannot be blocked. A lock is likely to cause a deadlock, since it does not allow marshaling for these components of threaded COM components. You block the thread using the block statement.

The CLR knows this requirement very well and does something. Blocking calls, such as Monitor.Enter (), WaitHandle.WaitOne / Any (), or Thread.Join (), pumps the message loop. The type of native Windows API that does this is MsgWaitForMultipleObjects (). This message loop sends Windows messages to keep the STAs active, including paint messages. This, of course, can cause redistribution problems, Paint should not be a problem.

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

Perhaps this all rings the bell, you probably can't help but notice that this is very similar to the application calling Application.DoEvents (). Probably one of the scariest methods available to solve UI freeze issues. That a fairly accurate mental model for what happens under the hood, DoEvents () also pumps up the message loop. The only difference is that the CLR equivalent is a bit more selective as to which messages it allows to send, it filters them. Unlike DoEvents (), which sends everything. Unfortunately, neither the Brumme message nor the SSCLI20 source is detailed enough to know exactly what is being sent, the actual CLR function that makes this unavailable at the source and too large to decompile. But, obviously, you can see that it does not filter WM_PAINT. It will filter real problem creators, add event notifications, such as a view that allows the user to close a window or click a button.

Function, not error. Avoid re-hits by removing blocking and relying on marching callbacks. BackgroundWorker.RunWorkerCompleted is a classic example.

+13
source

Good question!

All expectations in .NET "warn." This means that if the lock is idle, Windows can run Asynchronous Procedure Calls on top of the waiting stack. This may include processing some window messages. I have not tried WM_PAINT on purpose, but from your observations, I think it is included.

Some MSDN links:

Standby Functions

Asynchronous Procedure Calls

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

+3
source

I found this question having a question about locking the wait handle. The answers to this gave me a hint to implement the following:

  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). Eg 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; } } } 
0
source

Source: https://habr.com/ru/post/903371/


All Articles