Jono Jono - 26 days ago 5
C# Question

WinForms message loop not responsive

I'm deliberately abusing the message loop in a Windows Forms application, but my "just for fun" project quickly progressed beyond my level of understanding. While the task is running the form is unresponsive. Yes, there are lots of other questions like this, but in my case I am deliberately avoiding work on another thread (to win a bet against myself?)

I have a function that runs for (many) short slices of time on the UI thread:

get_IsComplete()
checks if the task is complete;
DoWork()
loops from 0 to 1000 (just to keep the CPU warm). The task is started up by calling
control.BeginInvoke(new Action(ContinueWith), control);
whereupon it (tail recursively) calls itself until completion, always running a short slice of work on the UI thread.

public void ContinueWith(Control control)
{
if (!IsComplete)
{
DoWork();
OnNext(control);
control.BeginInvoke(new Action(ContinueWith), control);
}
else
{
OnCompleted(control);
}
}


I expected the application to process other events (mouse clicks, control repaints, form moves etc.) but it seems my calls are getting more priority than I'd like.

Any suggestions?

Answer

The control.BeginInvoke() call places the delegate you pass in an internal queue and calls PostMessage() to wake up the message loop and pay attention. That's what gets the first BeginInvoke going. Any input events (mouse and keyboard) also go on the message queue, Windows puts them there.

The behavior you didn't count on is in the code that runs when the posted message is retrieved. It doesn't just dequeue one invoke request and executes it, it loops until the entire invoke queue is emptied. The way your code works, that queue is never emptied because invoking ContinueWith() adds another invoke request. So it just keeps looping and processing invoke requests and never gets around to retrieving more messages from the message queue. Or to put it another way: it is pumping the invoke queue, not the message queue.

The input messages stay in the message queue until the your code stops adding more invoke requests and the regular message loop pumping resumes, after your code stops recursing. Your UI will look frozen while this takes place because Paint events won't be delivered either. They only get generated when the message queue is empty.

It is important that it works the way it does, the PostMessage() call isn't guaranteed to work. Windows doesn't allow more than 10,000 message in the message queue. But Control.BeginInvoke() has no such limit. By emptying the invoke queue completely, a lost PostMessage message doesn't cause any problem. This behavior does cause other problems though. A classic one is calling BackgroundWorker.ReportProgress() too often. Same behavior, the UI thread is just flooded with invoke requests and doesn't get around its normal duties anymore. Frown upside down on anybody that runs into this: "I'm using BackgroundWorker but my UI still freezes".

Anyhoo, your experiment is an abysmal failure. Calling Application.DoEvents() would be required to force the message queue to be emptied. Lots of caveats with that, check this answer for details. The upcoming support for the async keyword will provide another way to do this. Not so sure if it treats the message priority any differently. I rather doubt it, Control.BeginInvoke() is pretty core. One hack around the problem is by using a Timer with a very short Interval. Timer messages also go on the message queue (sort of) but they have a very low priority. Input events get processed first. Or a low level hack: calling PostMessage with your own message yourself and overriding WndProc to detect it. That's getting a bit off the straight and narrow. The Application.Idle event is useful to do processing after any input events are retrieved.