• Open Menu Close Menu
  • Apple
  • Shopping Bag
  • Apple
  • Mac
  • iPad
  • iPhone
  • Watch
  • TV
  • Music
  • Support
  • Search apple.com
  • Shopping Bag

Lists

Open Menu Close Menu
  • Terms and Conditions
  • Lists hosted on this site
  • Email the Postmaster
  • Tips for posting to public mailing lists
Re: MPNotifyQueue takes 500,000 nsec (worst case)
[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

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.

  • Follow-Ups:
    • Re: MPNotifyQueue takes 500,000 nsec (worst case)
      • From: Kurt Revis <email@hidden>
  • Prev by Date: Kind of newbie question
  • Next by Date: ANN: AudioView Framework 1.0
  • Previous by thread: MPNotifyQueue takes 500,000 nsec (worst case)
  • Next by thread: Re: MPNotifyQueue takes 500,000 nsec (worst case)
  • Index(es):
    • Date
    • Thread