Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
303 views
in Technique[技术] by (71.8m points)

c# - 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.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...