Re: MPNotifyQueue takes 500,000 nsec (worst case)
Re: MPNotifyQueue takes 500,000 nsec (worst case)
- Subject: Re: MPNotifyQueue takes 500,000 nsec (worst case)
- From: kelly jacklin <email@hidden>
- Date: Tue, 22 Oct 2002 11:09:12 -0700
(I am not speaking for the CoreAudio group, but I have an intimate
understanding of the MP API implementation, have done a lot of
synchronization work on OSX, and have worked extensively with CoreAudio
APIs...)
You really do not want to use the MP API for this. MPNotifyQueue takes
a global (to the process) lock, in order to ensure the rather bizarre
termination semantics of the MP API. If this lock is held by any other
thread, your thread will priority invert with this thread, and you will
end up blocking until that thread has relinquished this lock. While
this is not usually a long time, it can be longer, depending on the
actions being performed by other threads in your app and the load on
the system.
In fact, when you are operating on the IOProc (which SoundInput
completions are), you want to do as few blocking operations as possible
(ideally none).
I suggest you use the pthreads APIs directly
(<
http://www.opengroup.org/onlinepubs/007908799/xsh/pthread.h.html>).
They are relatively easy to use, closer to the metal (the MP API is
implemented atop pthreads APIs), and the pthread_cond_signal call does
not block. Although to be used properly, it must be used in
conjunction with a mutex lock, which can contend with the thread on the
"other side" of the condition, it is possible through a combination of
timed waits (effectively polite polling) to avoid taking that lock, and
still ensure proper operation.
The best way I have found to do this is to have the IOProc post buffers
to a private list, using CompareAndSwap to push the buffer onto the
list, and then calling pthread_cond_signal to tickle the other thread
(if it happens to be waiting). In the meanwhile, the other thread is
doing a pthread_cond_timedwait, with a reasonable time interval to
catch the case where the pthread_cond_signal did not catch the thread
already asleep. When the consuming thread wakes up, it atomically
clears the entire list (again, using CompareAndSwap to set the list to
NULL and retain the old list pointer), reverses the order, and
processes all items in the list. Using the timeout version of the
wait-on-condition-variable allows you to not take the mutex on the
IOProc thread, which means you can truly avoid blocking on the IOProc
(a good thing). Since you do not take the lock, however, you need to
ensure that you do not block on the consuming thread for longer than it
would take to exhaust the buffers that the producer has available (you
also do not want to allocate memory on the IOProc, since that can block
against other calls to malloc within the app).
Buffers (several!) should be initially allocated and placed in a free
list by the app for use on the IOProc _before_ the IOProc starts
getting data. When the consume is finished with a buffer, it should
push the buffer onto a different "pending free" list using
CompareAndSwap, which list the IOProc can then migrate that entire list
(using CompareAndSwap as above to grab and clear the list) to the free
list, and use buffers as available. While the IOProc is running, only
the IOProc operates on the free list, so there is no need for
synchronization of the free list.
For the consumer's timeout, start with a rather small value (say the
anticipated wait for a buffer, e.g. 11ms (I think) if the IOProc buffer
size is 512 audio frames, at 44100Hz). The timeout should be adjusted,
based on how many buffers the IOProc had already prepared for the
consumer. If none, or only one, were available when the consumer woke
up, and it is acceptable to have more than one buffer of latency in the
system (i.e. you are capturing to disk, for example), then the timeout
should be longer. If many were available, then the timeout should be
shorter. Eventually, the timeout should reach a fairly steady value
that allows you to maximize the work-load you perform in a given loop,
but stay responsive to the IOProc. The consumer should _never_ let the
IOProc run out of buffers, obviously...
I also suggest you get off of the SoundMgr (which itself makes
potentially-blocking calls on the IOProc), and move to direct use of
the CoreAudio APIs. That way you can be sure of the behaviour of the
system.
The whole picture looks something like this (greatly simplified
pseudo-code...):
static buffer * free_list = NULL;
static buffer * pending_free_list = NULL;
static buffer * full_list = NULL;
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_mutex_t condvar = PTHREAD_COND_INITIALIZER;
io_proc(input_buffer, input_byte_size) {
if (free_list == NULL) {
do {
captured_free_list = free_list;
} while (! CompareAndSwap(captured_free_list, NULL, &free_list));
free_list = captured_free_list;
if (free_list == NULL) {
// this is the buffer exhaustion error case, which
// should not happen if the consumer is consuming
// at a reasonable pace...
return kAnError;
}
}
buffer_to_post = free_list;
free_list = free_list->next;
memcpy(buffer_to_post->data, input_buffer, input_byte_size);
do {
buffer_to_post->next = full_list;
} while (! CompareAndSwap(buffer_to_post->next, buffer_to_post,
&full_list));
pthread_cond_signal(&condvar);
return noErr;
}
consumer() {
while (io_proc_still_running) {
do {
captured_full_list = full_list;
} while (! CompareAndSwap(captured_full_list, NULL, &full_list));
if (captured_full_list != NULL) {
reverse(captured_full_list); // no sync required, since this is now
local
while (captured_full_list != NULL) {
process_buffer(captured_full_list);
processed_buffers += 1;
captured_full_list = captured_full_list->next;
}
}
pthread_mutex_lock(&mutex);
calculate_timeout(&timeout, processed_buffers);
pthread_cond_timedwait(&condvar, &mutex, &timeout);
pthread_mutex_unlock(&mutex);
}
}
Hopefully this is not too complicated, and can be easily followed, and
implemented with only slightly more pain. You will end up with a
system that is much more under your control, and has predictable
behaviour.
Hope this helps.
kelly jacklin
On Monday, October 21, 2002, at 9:43 PM, EJ Campbell wrote:
I'm having trouble reliably calling MPNotifyQueue from inside a
realtime thread on Mac OS 10.2. I'm using the queue to notify an
MPThread (operating at a lower priority) that a buffer of incoming
audio data is ready for processing.
I'm using the Sound Manager API, specifically SPBRecord with a Sound
Input Completion Proc, to record audio. My completion procedure is
called from within a realtime thread whenever a buffer of audio data is
ready for processing. In my completion procedure, I call MPNotifyQueue
to let my sound processing thread know that a buffer of data is ready
to be processed.
Most of the time everything works perfectly. However, occasionally, I
loose packets of audio. To track down the problem, I profiled my entire
input completion procedure. What I found was that occasionally the call
to MPNotifyQueue (or MPSignalSemaphore) takes a VERY long time to
complete. Normally the call takes approximately 1,500 nanoseconds.
However, during times when I loose audio packets, this call can take
upwards of 500,000 nanoseconds, which unacceptable when inside of a
record completion procedure.
_______________________________________________
coreaudio-api mailing list | email@hidden
Help/Unsubscribe/Archives:
http://www.lists.apple.com/mailman/listinfo/coreaudio-api
Do not post admin requests to the list. They will be ignored.