views:

309

answers:

4

I'm working on a heavily data-bound Win.Forms application where I've found some strange behavior. The app has separate I/O threads receiving updates through asynchronous web-requests which it then sends to the main/GUI thread for processing and updating of application-wide data-stores (which in turn may be data-bound to various GUI-elements, etc.). The server at the other end of the web-requests requires periodic requests or the session times out.

I've gone through several attempted solutions of dealing with thread-issues etc. and I've observed the following behavior:

  1. If I use Control.Invoke for sending updates from I/O-thread(s) to main-thread and this update causes a MessageBox to be shown the main form's message pump stops until the user clicks the ok-button. This also blocks the I/O-thread from continuing eventually leading to timeouts on the server.

  2. If I use Control.BeginInvoke for sending updates from I/O-thread(s) to main-thread the main form's message pump does not stop, but if the processing of an update leads to a messagebox being shown, the processing of the rest of that update is halted until the user clicks ok. Since the I/O-threads keep running and the message pump keeps processing messages several BeginInvoke's for updates may be called before the one with the message box is finished. This leads to out-of-sequence updates which is unacceptable.

  3. I/O-threads add updates to a blocking queue (very similar to http://stackoverflow.com/questions/530211/creating-a-blocking-queuet-in-net/530228#530228). GUI-thread uses a Forms.Timer that periodically applies all updates in the blocking queue. This solution solves both the problem of blocking I/O threads and sequentiality of updates i.e. next update will be never be started until previous is finished. However, there is a small performance cost as well as introducing a latency in showing updates that is unacceptable in the long run. I would like update-processing in the main-thread to be event-driven rather than polling.

So to my question. How should I do this to:

  1. avoid blocking the I/O-threads
  2. guarantee that updates are finished in-sequence
  3. keep the main message pump running while showing a message box as a result of an update.

Update: See solution below

+1  A: 

So you've got a complicated data-acquisition and processing chain that you want to keep running but then you insert a MessageBox in there. Nothing in the Threading+Invoke will change the fact that a MessageBox is Modal and that you have to wait for it to close, making the whole chain dependent on the User to click something.

So, get rid of the MessageBox, at least in the main path. If a segment of the processing does require user intervention then that segment must be on a separate thread.

Henk Holterman
The message box is displayed by a listener far down the datastream that listens for error message from a remote trading system and must be a popup. A possible solution could be to do the MessageBox.Show itself using BeginInvoke, it does not need to be synchronous with receiving the update.What confuses me is that MessageBox modality behaves differently when Invoked (main message pump stops) than when it originates from events inside the main thread or using BeginInvoke (message pump continues processing other events even while the MessageBox is shown).
Hans Løken
The message pump always keeps running for a Modal popup but the logic for event filtering is radically changed. But note that you can Control.Invoke a _function_ and wait for the result (SendMessage). And that is the difference with BeginInvoke, it uses PostMessage.
Henk Holterman
And Showing the MessageBox on another Thread requires a second Application.Run. It might be easier to use your own Form and use Show instead of ShowDialog.
Henk Holterman
Actually, I solved this problem by calling BeginInvoke on MessageBox.Show. This allows the update to finish before the message box is shown.
Hans Løken
+2  A: 

MessageBox itself pumps a message loop. That of course won't be the Windows Forms message loop. Everything runs as normal, but minus the dispatching of delegate invocation requests posted by Control.BeginInvoke(). Only the Windows Forms message loop can do that.

This happens when the MessageBox.Show() call is made on the UI thread. But not when it is made on a worker thread, message queues are a per-thread property. If you can get the Show call to be delegated to a worker, you probably solve your problem.

Addressing your questions:

  1. You really want the opposite: the worker threads should block. Not blocking can cause major problems, the BeginInvoke dispatch queue will fill up without bounds. One possible trick is to count the number of BeginInvoke calls, count down in the delegate target. Use the Interlocked class.

  2. The execution order of BeginInvoke targets is guaranteed. The real problem is probably related to having worker threads getting out of sync.

  3. Show the message box on a thread.

Hans Passant
1. The problem in this case is that the worker thread is the I/O-thread and blocking it will (eventually) time out the session on the server. Also, all updates received are critical in this application so they must be queued one way or another.2. You are correct that execution order for BeginInvoke is guaranteed but I don't think they are guaranteed to be atomic. For a more detailed explanation: http://www.codeproject.com/KB/cs/begininvoke.aspx3. I thought MessageBox always should be used in main/GUI-thread like other GUI
Hans Løken
The restriction is certain methods can only be run on the thread that the control's underlying handle was created on. does not have to be the main thread
Asher
Well, you're stuck between a rock and a hard place. I have to recommend you move the rock. Not sure what "atomic" is supposed to mean. Invoked delegate targets only run on one thread *and* execution order is guaranteed so there can never be an ordering or overlap problem. MessageBox works fine on a worker thread, but the box may get misplaced in the Z-order. NotifyIcon balloons are a better mouse trap.
Hans Passant
With "atomic" I mean that one BeginInvoke is guaranteed to have finished processing before the next one is started. This seems to not be the case when one BeginInvoke displays a MessageBox.
Hans Løken
+1  A: 

Don't use Forms.Timer to apply updates from the queue but use another thread to do it. This thread continually monitors the queue and (maybe) tells the GUI when to refresh itself with new data (via BeginInvoke) The MessageBox can be shown from this queue reader thread - does not have to be GUI thread.


Edit: The queue consumer can call Control.Invoke to display the messageBox to get around z-order issue

Asher
This is very similar to the solution I am currently implementing.
Hans Løken
so give a brother a up vote :)
Asher
Sorry, I was too late but gave your comment a vote. :-)
Hans Løken
A: 

Here is the solution I ended up with:

  • I/O thread puts all updates on a thread-safe/locking queue.
  • Separate worker-thread spins endlessly Dequeing updates and then BeginInvoke'ing them into the GUI-thread.
  • Display of MessageBox in GUI-thread in response to updates is now done with BeginInvoke.

This solution has the following benefits compared to the previous (described in 3. above using polling for GUI-updates):

  1. Event-driven update of GUI rather than polling. This gives both a (in theory) better performance and less latency.
  2. Neither GUI-updates nor I/O are locked by the Message Box.

Update: it seems that GUI-updates are still locked while a messagebox is shown using this solution. Will update when this is fixed.

Update 2: updated with fix for the worker-thread by changing Invoke to BeginInvoke.

Hans Løken
From your question you made it sound like the processing of the queue can only continue once the user clicks ok. Or that is how I read it
Asher
Sorry for the misunderstanding. This was not the case but it was important the message was modal and the user got it in a timely manner.
Hans Løken