0

I've offloaded a long-running, synchronous, operation to a background thread. It takes a while to get going, but eventually it starts producing items very nicely.

The question is then how to consume then - while maintaining a responsive UI (i.e. responding to Paint and UserInput messages).

One lock-free example sets up a while loop; we consume items while they are items to consume:

// You call this function when the consumer receives the
// signal raised by WakeConsumer().
void ConsumeWork()
{
   Thing item;
   while ((item = InterlockedGetItemOffTheSharedList(sharedList)) != nil)
   {
      ConsumeTheThing(item);
   }
}

The problem is that the background thread, once it gets going, can produce the items very quickly. This means that my while loop will never have a chance to stop. That means it will never go back to the message queue to respond to pending paint and mouse input events.

I've turned my asynchronous multi-threaded application in a synchronous wait as it sits inside:

while (StuffToDo)
{
   Consume(item);
}

Posting Messages

Another idea is to have the background thread PostMessage a message to the main thread every time an item is available:

ProduceItemsThreadMethod()    
{
   Preamble();
   while (StuffToProduce())
   {
       Thing item = new Item();
       SetupTheItem(item);
       InterlockedAddItemToTheSharedList(item);
       PostMessage(hwndMainThreadListener, WM_ItemReady, 0, 0);
   }
}  

The problem with this is that any posted message is always higher priority than any:

  • paint messages
  • mouse move messages

So as long as there is posted messages available, my application will not be responding to paint and input messages.

while GetMessage(out msg)
{
   DispatchMessage(out msg);
}

Every call to GetMessage will return a fresh WM_ItemReady message. My Windows message processing will be flooded with ItemReady messages - preventing me from processing paints until all the items have been added.

I've turned my asynchronous multi-threaded application in a synchronous wait.

Limiting the number of posted messages doesn't help

The above is actually worse than the first variation, because we flood the main thread with posted messages. What we want to do is only post a message if the main thread hasn't dealt with the previous message we posted. We can create a flag that is used to indicate if we've already posted a message, and if the main thread still hasn't processed it

ProduceItemsThreadMethod()    
{
   Preamble();
   while (StuffToProduce())
   {
       Thing item = new Item();
       SetupTheItem(item);
       InterlockedAddItemToTheSharedList(item);

       //Only post a message if the main thread has a message waiting
       int oldFlagValue = Interlocked.Exchange(g_ItemsReady, 1);
       if (oldFlagValue == 0) 
           PostMessage(hwndMainThreadListener, WM_ItemReady, 0, 0);
   }
}  

And in the main thread we clear the "ItemsReady" flag when we've processed the queued items:

void ConsumeWork()
{
   Thing item;
   while ((item = InterlockedGetItemOffTheSharedList(sharedList)) != nil)
   {
      ConsumeTheThing(item);
   }

   Interlocked.Exchange(g_ItemsReady, 0); //tell the thread it can post messages to us again
}

The problem again is that the thread can fill the list faster than we can consume it; so we never get a change to fall out of the ConsumeWork() function in order to handle user input.

As soon as ConsumeWork returns, the background producer thread generates a new WM_ItemReady message. The very next time i call GetMessage

while GetMessage(out msg)
{
   DispatchMessage(out msg);
}

it will be a WM_itemReady message. I will be stuck in a loop.

I've turned my asynchronous multi-threaded application in a synchronous wait.

Limiting ourselves to a count of items doesn't help

We could try forcing a break out of the while loop after, say, processing 100 items:

void ConsumeWork()
{
   int itemsProcessed = 0;
   Thing item;
   while ((item = InterlockedGetItemOffTheSharedList(sharedList)) != nil)
   {
      ConsumeTheThing(item);
      itemsProcessed += 1;
      if (itemsProcessed >= 250)
         break;
   }

   Interlocked.Exchange(g_ItemsReady, 0); //tell the thread it can post messages to us again
}

This suffers from the same problem as the previous incarnation. Although we will leave the while loop, the very next message we will recieve will again be the WM_ItemReady:

while (GetMessage(...) != 0)
{
   TranslateMessge(...);
   DispatchMessage(...);
}

that's because WM_PAINT messages will only appear if there are no other messages. And the thread is itching to create a new WM_ItemReady message and post it in my queue.

Pumping the message loop myself?

Some people cry a little inside when they see people manually pumping messages to fix unresponsive applications. So lets try manually pumping messages to fix unresponsive applications!

void ConsumeWork()
{
   Thing item;
   while ((item = InterlockedGetItemOffTheSharedList(sharedList)) != nil)
   {
      ConsumeTheThing(item);
      ManuallyPumpPaintAndInputEvents();
   }

   Interlocked.Exchange(g_ItemsReady, 0); //tell the thread it can post messages to us again
}

I won't go into the details of that function, because it leads to the re-entrancy problem. If the user of my library happens to try to close the window they're on, destroying my helper class with it, i will suddenly come back to execution inside a class that has been destroyed:

ConsumeTheThing(item);
ManuallyPumpPaintAndInputEvents(); //processes WM_LBUTTONDOWN messages will closes the window which destroys me
InterlockedGetItemOffTheSharedList(sharedList) //sharedList no longer exist BOOM

Down and down I go

I keep going in circles trying to solve the problem of how to maintain a responsive UI when using background threads. I've tinkered with four solutions in this question, and three others before asking it.

I can't be the first person to have used the Producer-Consumer model in a user interface.

How do you maintain a responsive UI?

If only i could post a message with priority lower than Paint, Input, and Timer :(

Community
  • 1
  • 1
Ian Boyd
  • 246,734
  • 253
  • 869
  • 1,219
  • The first question I would ask is, why does your GUI thread need to receive such a high-frequency stream of incoming messages? i.e. what is doing with those messages? Is it updating the GUI based on their contents? If so, then you could modify your producer thread to send no more than one message every 50mS or something, since you don't gain much by updating your GUI faster than 20Hz anyway. OTOH if most of the processing of the data is for non-GUI purposes, why do that processing in the GUI thread? – Jeremy Friesner Dec 07 '16 at 21:34
  • @JeremyFriesner I can do that too. I have batched up items into chunks of 1000, or only post a message every 100ms. But the same problem exists: i get a convoy of posted messages that prevent me from processing paint messages. By doing a batch, or a timed chunk, i only reduce the time my application is unresponsive - rather than solving the problem. – Ian Boyd Dec 07 '16 at 22:00
  • Let me restate the question... why do you require your GUI thread to do so much processing? Does it really need to process thousands of items (batched or not?) Can't that processing be done outside of the GUI thread? All the GUI thread should be responsible for is updating the GUI, and the complexity/expense of updating the GUI should be limited by the fact that the GUI can only show a limited amount of information anyway. – Jeremy Friesner Dec 07 '16 at 22:03
  • For example, as a worst-case scenario, your producer thread could just sent create a bitmap object and send that bitmap to the GUI thread, and the GUI thread's only responsibility would be to display that bitmap, verbatim. (I'm not saying that you should actually do that, though, it's just an example of how little a GUI thread actually *needs* to do) – Jeremy Friesner Dec 07 '16 at 22:06
  • The GUI thread doesn't require so much processing, it's placing the items *into* the UI. Imagine a Windows Explorer folder with 60,000 files. Having and handling, and the user using 60,000 files is not a problem. Getting the items out of the shared list and into the UI, while still processing paints is the problem. – Ian Boyd Dec 07 '16 at 22:07
  • Sounds like you might be hitting problems with data structures not scaling up, then. For example, if you're using a linked list and trying to keep it sorted, then each time you insert a new item into that linked list you're going to have to scan past an average of 30,000 other items to find the right spot to place the new item into... i.e. insertion into a sorted linked list is O(N), and insertion of N items into a linked list is therefore O(N^2), which is a great recipe for an abysmally slow program. You may want to profile your program and see where the hotspots are in the GUI thread. – Jeremy Friesner Dec 07 '16 at 22:10
  • Items are being added to the end of an array. The issue isn't CPU bound; the issue is that things run *too* fast. By the time the items are added to the UI, another batch of items is available to be consumed. If i intentionally slowed down the background thread - slower than the UI thread can accept produced work - the background thread would not get a chance to post a message of more pending items. – Ian Boyd Dec 07 '16 at 22:13
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/130050/discussion-between-jeremy-friesner-and-ian-boyd). – Jeremy Friesner Dec 07 '16 at 22:18
  • why not use a queue? Also I've heard user input and that ui should be one separate thread – marshal craft Dec 07 '16 at 22:32
  • Use a timer. Timer messages are low priority, so you won't risk clogging up the message queue, as you would with posted messages. – IInspectable Dec 07 '16 at 23:17
  • @marshalcraft In Windows, the thread that created the GDI objects must be the only one to manipulate them. That means that background thread producer can do a lot of the grunt work; but when it comes to updating the UI it must be done by the main thread. – Ian Boyd Dec 08 '16 at 14:59

0 Answers0