What's best practice for GUI change notification?

Short answer: No.

Long answer: Qualified No.

CallQueue and Listeners use FifoFreeStore as their ABA-immune allocator. The allocator has these properties:

  • Immune to the ABA Problem, without using 64-bit CAS
  • Memory is requested from the system in large blocks
  • Memory is typically recycled and not freed
  • A garbage collector reduces the “working set size” to the minimum number of blocks
  • Most allocations do not call malloc or take a CriticalSection.

When this memory allocator is first used there are several calls to malloc, and once the working set has a good handful of large blocks there are no more calls to malloc and free. Think of it as “priming the pump.”

The overhead for using a Listener or CallQueue is as low as possible, and suitable for direct usage in the audio device I/O callback. It’s been profiled and optimized extensively. If you have access to boost, the version with “Thread Local Storage” is superior. JUCE’s thread local storage is not as robust as the one in boost (which will soon be in all c+11 implementations). The difference is not very large though, you’ll likely not notice for an audio application (I tested the code using 64 threads and 1024 notifiers).

CallQueue::call is wait-free, runs in constant time plus the overhead of signal (a virtual function override), and takes no locks.

GuiCallQueue::signal incurs the overhead of an additional CallQueue::call, which it uses to notify an AsyncUpdater on a separate thread.

A call made on a ThreadWithCallQueue incurs the constant time overhead of CallQueue::call plus the cost of signaling a waitable event object.

Listener::call runs in O(N) where N is the number of different threads which have attached listeners. For example, if you have 10 listeners on 3 threads the runtime is O(3) (and not O(10)). This means that signaling the GUI thread runs in constant time no matter how many listeners are attached. The cost of multiple listeners is paid on the receiving end. In addition, Listener::call pays the signal price for each associated thread (depending on its virtual override).

The ManualCallQueue, which is used to call functions on the audio thread, has an empty body for signal which makes it cost almost nothing.