The dangers of buffering up posted messages and then reposting them later


A customer wanted to check that their idea for solving a re-entrancy problem doesn't have any hidden gotchas.

We have a window which processes incoming work. One of our work items enters a modal loop, and if new work gets posted to the window while the modal loop is running, our work manager gets re-entered, and Bad Things happen.

Our proposed solution is to alter the modal loop so that it buffers up all messages destined for the worker window. (Messages for any other window are dispatched normally.) When the modal loop completes, we re-post all the messages from the buffer, thereby allowing the worker window to resume processing.

The danger here is that reposting messages can result in messages being processed out of order. Depending on how your worker window is designed, this might or might not be a problem. For example, suppose that during the modal operation, somebody posts the WWM_FOO­STARTED message to the worker window. You buffer it up. When your modal operation is complete, you are about to post the message back into the queue, but another thread races against you and posts the WWM_FOO­COMPLETED message before you can post your buffered messages back into the queue. Result: The worker window receives the WWM_FOO­COMPLETED message before it receives the WWM_FOO­STARTED message. This will probably lead to confusion.

The place to solve this problem is in the window itself. That gets rid of the race condition.

LRESULT CALLBACK WorkerWindow::WndProc(
    HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
 if (uMsg is a work message) {
  if (m_cBusy) {
   // Now is a bad time to process the work message.
   // Queue it up for later.
   m_queue.Append(uMsg, wParam, lParam);
  } else {
   m_cBusy++; // prevent re-entrancy
   do {
    ProcessWorkMessage(uMsg, wParam, lParam);
   } while (m_queue.RemoveFirst(&uMsg, &wParam, &lParam));
   m_cBusy--; // re-entrancy no longer a problem
  }
  return 0;
 }
 ...  // handle the other messages
}

By queueing up the work inside the window itself, you ensure that the messages are processed in the same order they were received.

This technique can be extended to, say, have the worker window do some degree of work throttling. For example, you might keep track of how long you've been processing work, and if it's been a long time, then stop to pump messages for a while in case any system messages came in, and somebody is waiting for your answer.

Comments (18)
  1. John says:

    for a minute I thought this is a fun article about your scheduled batched posts auto-posting to this blog.

  2. Robert says:

      do {

       ProcessWorkMessage(uMsg, wParam, lParam);

      } while (m_queue.RemoveFirst(&uMsg, &wParam, &lParam));

    Doesn't this process the last message first?

    [The last message is also the first message. -Raymond]
  3. acq says:

    @Robert: The first in queue is the first that was added in the queue (First In First Out).

  4. Sean says:

    @acq no, I think he's right. The first iteration will process the message that was just received, and after that the loop will process all the messages in the queue, in FIFO order.

  5. Daniel says:

    @Sean: I've stumbled here as well at first. But as you can see, the whole idea of this code is to "block" processing further messages until the current message has been worked off:

    1. critical message is received

    2. Start blocking message processing

    3. Process the received (critical) message

    4. After this message has been processed, work off the queue (FIFO)

    5. Once we worked off the queue we unblock message processing again.

  6. A.C. says:

    @Daniel. In spirit I think this is what should happen, but what prevents the following race conditions?

    1. Two callers of this function check m_cBusy and decide it is 0.

      Maybe this can't happen because ProcessWorkMessage() has to pump the additional messages that would lead to WndProc() being called again?

    2. Queue is empty so loop stops, but someone is just about ready to insert message into queue. Next time messages will be processed out of order with uMsg first and then queued message. This might be what Robert/Sean were seeing.

      Is there something that prevents this from happening?

    [Windows are single-threaded objects. -Raymond]
  7. Mark says:

    Or to explain Raymond's cryptic response, the last message is the first message because you can only enter the processing loop when the queue is empty.

  8. Mark says:

    A.C.: that can only occur if two threads are trying to run the WndProc, in which case all sorts of badness happens.

  9. Billy O'Neal says:

    When I first looked at this I thought you were talking about buffering *blog posts*. :)

  10. GregM says:

    "for a minute I thought this is a fun article about your scheduled batched posts auto-posting to this blog."

    "When I first looked at this I thought you were talking about buffering *blog posts*. :)"

    Well, at least I wasn't the only one.  :)

  11. Deduplicator says:

    Does that single-threadedness still hold if we attach input queues? I thought all hell would break loose…

    [The input queues are attached but each window still belongs to its respective thread, as we saw earlier. Imagine the havoc if you had cross-process input queue attachment – would this mean that window procedures started running in another process? -Raymond]
  12. Anonymous Coward says:

    Another interesting problem you can get with reposting messages that I once saw happen in an MFC application is that horrible things might happen when the number of messages you might have to repost knows no bound.

    What I observed was that after exiting the guarded code, which was sometimes entered after processing a reposted message, the application eventually spent most of its time processing and reposting messages, degrading user input responsiveness until it eventually locked up.

  13. Joshua says:

    Amazing how a basic lack of understanding can come to a conclusion that this horrendous kludge can solve the problem, and end up almost solving the problem so you don't know it's wrong until too late.

  14. acq says:

    So Sean and Robert point something: the do while first processes the msg from the arguments of WndProc and only then all from the queue. The response from Raymond is "The last message is also the first message" and Mark explains "the last message is the first message because you can only enter the processing loop when the queue is empty."

    Now I still don't get it why it's OK to process the msg from the arguments of WndProc before all the messages from the queue. Anybody has a bit clearer explanation than those cited?

  15. Bart says:

    @acq

    Let's say the queue is empty. We get a work message. We set our status to busy and process the message. If new work messages are posted during this time, since our status is busy, they are added to the queue. Only when the queue is completely empty, do we set our status to not busy. The next time a message is received, it is processed immediately, which is fine because the queue is empty.

    Whenever entering that part of the code, "the last message is also the first message", because when there is only one item in a queue, that one item is both the first and last message.

  16. acq says:

    Thanks Bart and Mark, I was confused by the mention of "a modal loop" and the "another thread races" etc. I understand what the wndproc code assumes now, only the whole context is still a bit blurry: Is the existence of other threads important or would the whole story be a non-issue if all the messages come from the single thread? Ditto for the modal loop. Thanks.

  17. Mark says:

    acq: you need to process the msg from the arguments before everything in the queue because the queue is by definition empty (m_cBusy == 0). Messages only start queueing up once you've incremented m_cBusy (safe because we're single-threaded), and so all of them arrived *after* the one we're processing.

  18. Mark says:

    acq: the WndProc will only be called by the owning thread, but that doesn't mean it's the only thread *posting* messages. The thread race is down to the fact that they were trying to re-post the message, at which point they're working with a multi-threaded object (the window's queue).

    As for the modal loop, there can always be re-entrancy if ProcessWorkMessage results in a call to SendMessage for the very same window. The window manager will realise we're in the right thread and simply call the WndProc again (so you'll see it twice in a stack trace).

Comments are closed.