Multithreading and GUI visual skips


#1

I’m on JUCE v2.0.18, Mac, running Snow Leopard.

I’ve been working on a multithreaded app for a while, which I’ve posted questions about here and gotten good feedback on. My app is basically reading data from the serial port and plotting it on the screen like an oscilloscope.

At this point, I’ve disabled most of my threads for debugging purposes. Literally all that I have going at the moment is this:

  1. The message manager thread has a timer running every 20 ms that takes the “latest” data set from my “data-collating thread” and plots it visually
  2. My “data-collating” thread sets up an initial dummy data set, and then keeps it as is and does nothing but this: while(true) { Thread::sleep(10000) }

So as you can see, the net behavior here is that the message manager thread just keeps plotting the same thing over and over and over, and every 10 seconds the data-collating thread comes back up and immediately returns to sleep.

In the above setup, the GUI behavior ends up getting very visually “choppy.” If I move my mouse over a button, for instance, it’ll take a second for the button to switch to the darker “mouse over” color, and if I click a button it takes a second for it to register. It’s very visually disorienting and feels as though the program has “hung” for a second.

If I change Thread::sleep(10000) to Thread::wait(10000), the same problem happens - no difference. However, if instead I put Thread::wait(-1) in the while loop, then the problem goes away, and everything is really fast again.

Does anyone have any insight into what might be causing this problem? Is there something fundamental about JUCE threads that I’m not understanding? I suspect that it has something to do with the Message Manager thread implicitly locking things while it does the painting, but I’m not sure…

Thanks, any insight would be much appreciated!


#2

20 mS would be 50 Hz. Depending on what/how you are drawing, you could be starving the system. I do this sort of acquisition/display all the time. Typically I’d do something like:

data thread:
Yield until new data
store/validate/whatever
Signal a WaitableEvent

DisplayThread:
Wait on WaitableEvent
check for shouldexit (so you can shut down gracefully)
Take the message lock (if can)
Update UI
Make sure lock is released (goes out of scope)
Sleep (50)
repeat

The sleep insures that my update rate to the screen is held to 10-20 Hz and the rest of the system has time to respond (there are other techniques, this is just a simple example).

You also want to confirm that what you are painting is held to a minimum.

All that said, it’s a wild guess. There are so many opportunities to hose yourself in multithreaded programming it’s difficult to debug based on your simple description. If you share resources between threads, you generally need to use an atom or lock to make sure that one side isn’t seeing a partially updated (corrupt) version. Once you use atoms/locks, the opportunity for deadlocks comes into play.


#3

Another choice is to have an AsyncUpdater in the gui thread, and call triggerAsyncUpdate() from the other thread (e.g. audioDeviceIOCallback).

True!!!

An alternative is to atomically provide each thread with read-only, reference counted copy of the data. When the owner wants to update it can create a new object, duplicate it, and pass it to the readers with a thread queue. This eliminates locking and any possibility of deadlock at the expense of making copies.


#4

Many thanks for the replies in this thread - I’m going to mess with Vinn’s suggestion to implement a ThreadQueue. Also much thanks to Vinnie for the help in #Juce too. Definitely will be idling in there from now on.


#5

A very good choice, though it seems to wrap a similar mechanism for you.

[quote=“TheVinn”]
An alternative is to atomically provide each thread with read-only, reference counted copy of the data. When the owner wants to update it can create a new object, duplicate it, and pass it to the readers with a thread queue. This eliminates locking and any possibility of deadlock at the expense of making copies.[/quote]

Data duplication is another viable method of achieving thread safety, but it doesn’t seem to really eliminate semaphore mechanisms. True, they can be atomics, but the need is still there. The reference counting needs to be thread safe, the memory manager needs to be thread safe (in case the reference reaches 0 in another thread), and the Queue head/tail mechanism needs to be thread safe.

My point is just to stress that it isn’t the elimination of ‘locks’ that makes the setup immune to deadlock, but the data model.


#6

For reference all of the concepts related to thread queues and what not are part of VFLib:

VFLib hosted on Github

Listeners
CallQueue
ManualCallQueue
GuiCallQueue
ThreadWithCallQueue