Monday 28 December 2009

iPhone - Posix Threads for the win

So I've been looking into multithreading my iphone engine. Switching from using a timer to threads bumped up my frame rate by about 10% instantly. I found that using Posix threads were slightly (1-2%) faster than NSThreads. Currently, I've set my engine up to have 2 additional threads. One to handle the game updating and rendering, and a second to handling job loading, with jobs being streaming in audio and textures. To handle synchronisation I'm using spin locks, which are basically while loops.

Things to note
  • When setting up your thread you must always, start and release an NSAutoRelease pool.
  • The pool should be restarted every now and then to release the objects released during the runtime of the thread.
  • When using Posix threads it's recommended you run an empty NSThread just to let the OS know that the app will be multithreaded.
  • Setting up an OpenAL context stalls the engine by around half a second on start up, so I tend to launch it in the jobs thread to avoid stalls.
  • Bumping up the priority of the render thread does improve performance slightly.

Sample code
When called the start function will launch a dummy thread which does nothing, so the thread will terminate immediately. It then launches two threads, a game thread and a jobs thread. The game thread is given a higher priority than the jobs thread.

-(void)start

{

// iPhone SDK recommends launching an empty NSThread when using POSIX threads with Cocoa applications

[NSThread detachNewThreadSelector:@selector( emptyThread ) toTarget:self withObject:nil];

// Create the game thread using POSIX routines.

createThread( &PosixGameThread, self, 0, +2 );

// Create the jobs thread

createThread( &PosixJobsThread, self, 0, -2 );

}

Empty thread does nothing, so the thread will terminate naturally upon launching.

-(void)emptyThread

{

}


Create Thread wraps the PSIOX routines required to launch a thread with a set priority.

void createThread(void *(*start_routine)(void*), void *restrict arg, int prioritySet, int priorityAdj)

{

// Create the game thread using POSIX routines.

pthread_attr_t attr;

pthread_t posixThreadID;

int returnVal;

returnVal = pthread_attr_init( &attr );

assert( !returnVal );

returnVal = pthread_attr_setdetachstate( &attr, PTHREAD_CREATE_DETACHED );

assert( !returnVal );

returnVal = pthread_create( &posixThreadID, &attr, start_routine, arg );

assert( !returnVal );

struct sched_param param;

int policy;

pthread_getschedparam( posixThreadID, &policy, &param );

assert( !returnVal );

if( prioritySet != 0 )

{

param.sched_priority = prioritySet;

}

else if( priorityAdj != 0 )

{

param.sched_priority += priorityAdj;

}

assert( param.sched_priority > 0 && param.sched_priority < 100 );

returnVal = pthread_setschedparam( posixThreadID, policy, &param );

assert( !returnVal );

returnVal = pthread_attr_destroy( &attr );

assert( !returnVal );

}


The game thread owns the OpenGL context and spins around the while loop updating the frames while the game is running, while making a call lazily to refreshReleasePool to restart the pool.

void* PosixGameThread(void* data)

{

// Note: autorelease pools should be created and released more frequentley if using this feature

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

uint poolRefreshCounter = 0;

EAGLView *view = (EAGLView*)data;

[view setupGameThread];

[pool release];

pool = [[NSAutoreleasePool alloc] init];

view->runningGame = YES;

while( view->runningGame )

{

if( view->paused == NO )

{

[view update];

#if DEBUGON && TARGET_IPHONE_SIMULATOR

// 20 frames a second in debug

usleep( 50000 );

#endif

}

refreshReleasePool( &pool, &poolRefreshCounter, 100 );

}

[view shutdown];

[pool release];

return NULL;

}


The jobs thread works the same way as the game thread, but owns the OpenAL context instead and starts the file I/O operations requested by the game thread.

void* PosixJobsThread(void* data)

{

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

uint poolRefreshCounter = 0;

EAGLView *view = (EAGLView*)data;

while( view->runningGame == NO )

{

usleep( 1000000 );

}

[view->audioManager load];

usleep( 200000 );

while( view->runningGame )

{

if( [view startJobs] )

{

// 5 jobs a second

usleep( 200000 );

}

else

{

usleep( 1000000 );

}

refreshReleasePool( &pool, &poolRefreshCounter, 10 );

}

[pool release];

return NULL;

}


Refresh Release Pool handles, reseting the NSAutoreleasePool lazily for both threads.

void refreshReleasePool(NSAutoreleasePool **pool, uint *count, const uint target)

{

if( (*count)++ > target )

{

[*pool release];

*pool = [[NSAutoreleasePool alloc] init];

*count = 0;

}

}


Any questions? Feel free to ask.

No comments:

Post a Comment