News
Fun With Threads
Posted on 29 July 2008 by
This weekend I started working on a replacement for MultimediaKit. This has been on my TODO list for a while, since the current one is GPL-tainted. I started working with libavcodec and libavformat directly, since these are LGPLd.
In order to get consistent latency, ideally I wanted the decoder running in its own thread. Since we have a threading library in svn, I thought I'd try using it (okay, I wrote it, but I've not actually had the need of a threading framework since then). The first thing I needed to do was create the player object and put it in its own thread:
MusicPlayer *player = [[MusicPlayer alloc] initWithDefaultDevice];
// Move the player into a new thread.
player = [player inNewThread];
Actually, that's all I needed to do. After putting some files in the player's queue, I could periodically query its state, like this:
// Periodically wake up and see where we are.
while (1)
{
id pool = [NSAutoreleasePool new];
sleep(2);
NSLog(@"Playing %@ at %lld/%lld", [player currentFile],
[player currentPosition] / 1000, [player duration] / 1000);
[pool release];
}
Note the complete lack of any locking or thread operations here. The player
object, after the call to -inNewThread
is really a proxy which maintains a lockless ring buffer storing messages between the player and my main thread. When I send it a currentFile
message, it adds it to the queue and returns a proxy. If I try to use the proxy (here, NSLog
will do so by sending it a -description
message) then my calling thread will block. The other two messages return primitives, so they block immediately.
When I am not sending the player messages, the run loop managed by EtoileThread sends it a -shouldIdle
message whenever the message queue is empty, and if it is then it sends it an -idle
message. The -idle
method reads the next frame from the audio file, decodes it, and passes it to the output device. All of these are synchronous, blocking, calls (although the output device does some buffering) and so it's very simple code. Neither thread needs to spend much time waiting on a mutex - the structure used to send messages between threads is a hybrid ring buffer, which runs in lockless mode unless it has spent a little bit of time spinning (at which point it uses a mutex).
This means that, while playing, the cost of checking for new messages is very cheap (one comparison operation, in fact). While paused (and not receiving messages), the object will automatically switch to locked mode and wait for a condition variable to wake it up, so you aren't wasting CPU.
The best thing is that all of this is hidden away in EtoileThread (in EtoileFoundation), so any of your objects can use the same mechanism with almost no code. Just adopt the Idle
protocol if you want to do something when your object isn't receiving messages from another thread, and send it an inNewThread message just after creation.