Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

When do I need a lock-free data structure for reading/writing data across threads in audio apps?

My scenario is something like: User interacts with GUI elements, audio callback function reads variables set by the UI, calculates samples and stores samples in a buffer (or whatever data-structure), buffer is then read by the UI and draws a wave form (in a draw loop 60 times per second).

Now, according to some stuff I've read (a thread in the Linux audio dev list, this and this) I need some sort of data structure that can be read and written simultaneously without requiring a lock, or, I need some sort of cross-thread notification system to pass variables around.

However, some examples I've seen use vanilla vectors from the C++ std library, they read from one thread and write from the other and when I run the programs, they run fine.

  1. In which cases do I need to use a lock free data structure do do this sort of cross-thread communication?
  2. If I add another thread such as a MIDI or OSC callback function that receives network IO and needs to pass data to the other two threads, do I need to worry about lock-free structures?
  3. Which would be an adequate structure to use if answer to number two is "yes"?
like image 426
Rafael Vega Avatar asked Feb 13 '12 03:02

Rafael Vega


1 Answers

If you have threads that are accessing the same memory (reading or writing), then you either need to use locks or you need to use a lock-free data structure. Otherwise your data structures can become corrupted (or appear corrupted) when they are being accessed by more than one thread at the same time.

It appears that the examples you are pointing to use a fixed-size vector that is allocated ahead-of-time. The audio thread is writing to this buffer and the UI thread is reading it, and the two are not synchronized. Since the two could be running completely concurrently, the UI thread has no guarantees about what data is actually being read; it might read some data from update N and some from update N+1. It might miss some data or read some data twice (or more). This is not a robust way to build audio applications. It will "work" well enough for a simple visualization app because the results of the visualization don't need to be perfect, but it would be totally unsuitable for a recording or playback application.

Audio applications often use lock-free data structures (instead of using locks) because audio playback has "real-time" requirements. If your audio buffers contain 100ms of sound, then you need to fill these buffers 10 times a second or else your audio playback will stutter. Another way of saying it is that you have a 100ms deadline to fill the buffer each time.

There are all sorts of things that can cause you to miss this 100ms deadline; if the system is too busy your process might not get scheduled, or a page fault could cause an disk read that blocks the process for too long, just to name a couple of examples. If you're attempting to acquire a lock but another thread holds it for more than 100ms, this will make you miss your deadline. This is why using locks can be bad for audio applications; another thread that holds the lock for too long can make you miss your deadline.

With a lock-free data structure there is no lock to wait for, so other threads cannot halt your progress. It makes it easier to make your audio I/O deadlines.

But before you get too excited about lock-free algorithms, you should know that they're much more subtle and require much more expertise to use correctly than locks. Basically if you're not a specialist in this area you should not attempt to write lock-free algorithms yourself. Perhaps there is a good open-source library that has some lock-free algorithm implementations; I haven't looked lately.

But realize that the extra work it takes to use lock-free algorithms is more or less wasted unless you are also being extremely careful in your audio thread to avoid other possible causes of delay. Specifically:

  • your audio thread must not malloc() or free() any memory (most malloc/free implementations acquire a global lock internally)

  • you must make sure that any memory accessed by your audio thread is not paged out (with eg. mlock())

  • your audio thread must not perform any I/O besides with the sound card, since I/O calls can block.

Unless you are being very diligent and taking all these steps, you might as well use a lock for data that is shared with other threads, but do be sure that the lock is held for very short amounts of time. For example, be sure not to perform any malloc()/free() or blocking system calls in the UI thread with the lock held.

like image 156
Josh Haberman Avatar answered Oct 06 '22 00:10

Josh Haberman