Using lager in an audio app, and lock-free/wait-free constraints implications #219
Unanswered
nebularnoise
asked this question in
Q&A
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
This is a follow up on a discussion in the immer repository, related to avoiding blocking a real-time audio thread.
My mental model of state
I just want to preface my questions with this paragraph, to disambiguate some terms I might use.
After reading the docs, and exploring the usage of lager, I've mentally identified three categories of state, that I name:
lives in the store, saved to disk.
e.g.
DocumentModellives in the store, but not saved to disk.
e.g.
ewig::application.window_sizelives outside of the store, is almost an implementation detail of a system
e.g.
lager::examples::todo::ui_state.new_todo_inputLet me know if there is a more suited and well-established nomenclature.
Reading the store from the audio-thread
For reference, I'm concerned about reading the global state, or part of it, from a real-time audio thread.
What I'm unsure about is the thread-safety and wait-free-ness of
store.get()and/or cursor evaluation and getters. I feel like there are potential footguns I'm not seeing.I've thought about:
immer::atomimmer::atomimmer::atomto part of the stored stateOption 1 and 2 would look like this:
For these options, I'm not sure getting state from the audio thread (
store.get().load().dsp_modelfor option 1 /store.get().dsp_model.load()for option 2, and/or their equivalent cursor counterparts) would even be safe to do in a thread safe way.Does that make sense ?
For that reason, my gut tells my option 3 is probably best, and does not try to shoe-horn wait-free constraints on the framework as a whole. It would look something like:
Is this sort of design the correct way to go?
Mutating state in the audio thread
a. "private" transient state within the audio thread
Let's consider a simple sine oscillator.
I'd qualify
frequencyas part of the model, but notphase.I'm basing my intuition on this example:
To me,
phaseis transient, and only useful to the audio callback, and does not need to be shared. My understanding is thatfrequencyshould live in the store, butphaseshould be kept out of the store. Does that make sense?b. shared transient state, mutated in the audio thread
Let's consider an audio level meter.
Only the audio thread is able to compute the volume of incoming audio, but the GUI thread is in charge of displaying that information. The audio thread needs to share the computed level in dB with the GUI somehow.
This 'level' information could be a single instantaneous value, represented by a
double, or a history of such values, represented by avector<double>. Should this block of memory live in the store?My understanding is that similarly to the example above, this is considered to be a "transient state", it needs to live outside the store, and regular wait-free mechanisms should be used to share these computations between the audio thread and the main GUI thread.
c. intent generated from the audio thread
Let's consider a sample player, which has a
onPlaybackFinishedcallback (in the business logic sense) that should mutate the model. My instinct would be to use wait-free message-passing to mutate the state from the event loop instead of dispatching actions from the audio thread.In other words:
d. Separate stores for "real-time business logic" and "non-real-time business logic"
One could imagine a separate "real-time store" to handle the state of inter-operating time-critical subsystems, and the mutation of that state within the real-time thread.
This "real-time store" could then communicate with the "non-real-time store", via message-passing.
However, because lager uses dynamic allocation and mutexes under the hood, this 'real-time store' cannot be a
lager::store. It would have to be some sort of similarly implemented "unidirectional data flow" system, with zero locks and zero dynamic allocation.Is this a correct assumption?
If so, one could imagine to augment lager to use a 'Strategy' template type parameter to customize the implementation of the mechanisms which currently are hardcoded to use
std::shared_ptr,std::functionandstd::mutex. Many boost libraries use this sort of strategy pattern, as far as I know.If I ever reach a point where my audio callback grows enough in complexity to warrant that, would that be an interesting contribution?
Beta Was this translation helpful? Give feedback.
All reactions