7

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?

Jono
  • 1,964
  • 4
  • 18
  • 35
  • Hans is right on the money, you get nowhere like this, execution never leaves the function. What I was thinking about saying, you can avoid it by using a timer but he called that too! Don't use invoke / BeginInvoke like this. Pretty senseless to do it outside of multithreading situations anyway; you can just call the code directly. And before you go on DO follow his link; it's a very concise explanation. Most 90's area C windows API programmers would write 20 paragraphs on this (my most impatient user thanks you, Hans). – FastAl Jul 22 '11 at 14:11

2 Answers2

21

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.

Community
  • 1
  • 1
Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • 1
    Hans, thanks for taking the time to give such a detailed answer. You are right to point out that my misunderstanding was the behaviour of the message processing loop to empty the queue before moving onto painting. With an answer like this it was far from a dismal failure, and I've since created a toy WPF app (as per Robert Levy's suggestion, below) to avoid calling Application.DoEvents(). Could the WPF model ever be considered an OK way of avoiding threads, or will it always be shunned by the stackoverflow high-rollers like yourself? – Jono Jul 22 '11 at 14:14
  • 2
    I personally avoid threads whenever I can, way too many scars. I reach for state machines whenever possible, what the *async* keyword is doing under the hood. Pertinent to the kind of programming I do (machine control). And not a WPF fan, don't know the exact details of Dispatcher.BeginInvoke() well enough to follow up. You could start another question about it. – Hans Passant Jul 22 '11 at 14:59
1

Use the begininvoke overload that takes a priority. The 'normal' priority is higher that input and rendering. You need to choose something like applicationidle

Robert Levy
  • 28,747
  • 6
  • 62
  • 94
  • Errr... I was thinking WPF and dispatcher.begininvoke. With winforms in think emiswelt is right that you are stuck with ugly DoEvents calls – Robert Levy Jul 22 '11 at 11:35
  • I don't see any public overloads of System.Windows.Forms.Control.BeginInvoke that take a priority as a parameter. Where can I find them? – Jono Jul 22 '11 at 11:40
  • Ah, ok, thanks. If WPF offers this method then I may attempt a WPF version of my experiment for v0.2! – Jono Jul 22 '11 at 11:46
  • I'm impressed with the way WPF allows me to do this; it works better IMHO than the WinForms way. The code I used was: control.Dispatcher.BeginInvoke(new Action(ContinueWith), DispatcherPriority.Background, control); – Jono Jul 22 '11 at 14:02