Torque/1.4/MultiThread

From TDN

Introduction

Torque 1.4 adds a lot of improvements in terms of thread safety, but multithreaded programming with Torque is still a minefield. To get anything out of this article besides premature baldness, all of the following should apply to you:

  • You are an experienced C++ programmer.
  • You are experienced with the debugger. printf() debugging does not cut it for debugging multithreaded code.
  • You are experienced with multithreaded programming. Torque is not a good codebase to learn multithreaded programming on.

Do not take this warning lightly. Without the neccessary experience all you will be able to produce is code that works sometimes but crashes in strange and seemingly impossible to debug ways. Threads are not a holy grail, they require a completely different mindset to single threaded programming.

Contents

Setting up Torque

Torque is probably set up correctly out of the box, but it's worth double checking.

  • Ensure TORQUE_MULTITHREAD is defined in core/torqueConfig.h. Some of the Console and Sim mutexes are disabled when TORQUE_MULTITHREAD is not defined. However, the thread related classes will still be available regardless.
  • Windows: Make sure Torque and all libraries you are linking to are compiled with a multithreaded version of the runtime. If you have to change that, make sure you rebuild all afterwards, including the libraries.
  • Mac and Linux: On the Mac and Linux, Torque uses pthreads to implement threading. You should ensure all code is compiled with the -pthread GCC option.

Thread Safe Torque Code

The following list is not intended to be exhaustive, so treat it as a guide.

You should carefully check all code that you call from a thread, either directly or indirectly. As a rough rule of thumb, if there are references to mutexes or semaphores in the code you are reading then Torque is probably thread safe up to that point and you dont need to check any deeper into the call tree. Making checking a habit will save you some hair.

Torque Core

The Memory Manager and Profiler are both threadsafe.

Sim

SimNameDictionary, SimManagerNameDictionary, and SimIdDictionary are threadsafe. This allows you to post Sim Events and use the various Sim::findObject() functions from any thread. It is also worth noting that Sim Events will always execute in the main thread.

You can create, register and delete SimObjects from any thread safely. Furthermore, SimSet and SimGroup are safe to use from any thread.

Executing Script Code

It is important to note that the script compiler and interpreter are NOT thread safe. However, you can still execute script code from a thread. When calling Con::execute(S32 argc, const char *argv[]) or it's equivalent Con::executef() from a thread, Torque posts a SimEvent that causes the main thread to execute the script function on the next tick. The calling thread will be blocked until the script has finished executing to allow the script to return values.

No other forms of Con::execute() are currently threadsafe. This means you cannot use the object method version of it from a thread. Furthermore, all forms of Con::eval() are not threadsafe. If you need to call these functions from a thread, you will either have to threadsafe them yourself (please submit patches if you do!) or rethink your code.

Torque Thread Classes and Functions

The Torque platform layer provides a set of cross platform classes for Threads, Mutexes and Semaphores.

Threads

To create a new thread in Torque, you have two options. You can either use a static function, or subclass Thread and override the run() virtual method.

platform/platformThread.h defines the Thread class, and is used for both methods of creating threads.

Static Functions

Your function must use the ThreadRunFunction prototype, defined as follows:

typedef void (*ThreadRunFunction)(S32);

To start the thread, you would use the following code:

static void threadFunc(S32 arg)
{
	// Thread code here ...
}

S32 arg = 0;
Thread *t = new Thread(threadFunc, arg);

The Thread constructor specifies defaults for all it's arguments, so if you dont need to pass an arg to the thread function, you can leave it out.

Overriding run()

An alternative, and generally more useful, method of creating threads is to subclass the Thread class and override the run() virtual method. Starting a thread is the same as before, except you specify 0 as the first argument to the Thread constructor. For example:

class ExampleThread : public Thread
{
public:
	ExampleThread() : Thread(0)
	{
	}

	virtual void run(S32 arg = 0)
};

void ExampleThread::run(S32 arg)
{
	// Thread code here ...
}

The arg parameter is generally not needed when subclassing, as you can specify data for the thread more easily as protected or private member variables.

Starting and Stopping Threads

By default, the Thread constructor starts the thread automatically. This behavior is specified by a third argument to the Thread constructor. Set it to true (the default) to start the thread when its constructed, or false to delay starting the thread. If you have delayed starting the thread, you can start it with the start() method.

Stopping threads is slightly more tricky. The join() method causes the calling thread to block until the thread exits, but there are no allowances for actually stopping the thread. Therefore, you have to provide your own means of signalling the thread to exit.

It is also worth noting that the Thread destructor calls the join() method, which can cause annoying deadlocks if you don't take care at exit.

Mutexes

Mutex abstraction is defined in platform/platformMutex.h as static methods on the Mutex class.

Working with Mutexes

To create a mutex, use:

void *mutex = Mutex::createMutex();

To destroy it, use:

Mutex::destroyMutex(mutex);

Locking and unlocking the mutex is similarly easy:

if(Mutex::lockMutex(mutex))
{
	// Access protected data ...
	Mutex::unlockMutex(mutex);
}

By default, locking the mutex will cause the calling thread to block until the mutex becomes available. If you dont want the thread to block, you can specify false as the second argument to lockMutex(). For example:

if(Mutex::lockMutex(mutex, false))
{
	// Access protected data ...
	Mutex::unlockMutex(mutex);
}

Making your life easier

When working with mutexes, it is often the case that you need to return a value while the mutex is locked. Usually you have to copy the data, unlock the mutex then return the copy. This gets really annoying really quickly. Enter MutexHandle, a simple class that makes these situations easy to deal with.

Instead of locking a mutex normally, you create a MutexHandle on the stack and use that to lock the mutex. When the function returns, the MutexHandle goes out of scope and the destructor unlocks the mutex for you. Here's a simplified example:

Mutex *gMutex = Mutex::createMutex();
bool gMutexProtectedData;

bool annoyingFunction(void)
{
	MutexHandle handle;

	if(handle.lock(gMutex))
	{
		return gMutexProtectedData;
	}

	return false;
}

Semaphores

Semaphores are defined in platform/platformSemaphore.h and are abstracted similarly to mutexes.

To create a semaphore, use:

void *sem = Semaphore::createSemaphore();

You can also specify the initial count as an argument to createSemaphore(), which defaults to 1.

To destroy it, use:

Semaphore::destroySemaphore(sem);

Acquiring and releasing the semaphore is similarly easy:

if(Semaphore::acquireSemaphore(sem))
{
	// Access protected data ...
	Semaphore::releaseSemaphore(sem);
}

As with mutexes, locking the semaphore will cause the calling thread to block until the semaphore becomes available. If you dont want the thread to block, you can specify false as the second argument to acquireSemaphore().

If you are feeling lazy, you can use Semaphore::P() in place of Semaphore::acquireSemaphore() and Semaphore::V() in place of Semaphore::releaseSemaphore().

Con::isMainThread()

The console provides a Con::isMainThread() function, which returns true if it's called from the main thread or false otherwise. Currently, this is used by Con::execute() to determine whether it can call script code normally or if it needs to use a Sim Event.

Example Thread Code

The new Theora code uses a thread for playing the video.