Threads

From Valve Developer Community
Jump to navigation Jump to search
Abstract Coding series Discuss your thoughts - Help us develop the articles or ideas you want

Levels & XP | Optimization | Procedural Textures | Sights & Sniperrifles | Special effects | Vehicles | Threads | Save Game Files | Night Vision | Non-offensive Weapons | Dynamic Weapon Spawns | Dynamic Weapon Spawns (Advanced)

Threads are code execution paths that run simultaneously. On single-core systems this is achieved by switching rapidly between threads, while on multi-core systems things really do happen at exactly the same time.

As of 2024, less than 0.07% of systems were single-core, and that figure is decreasing all the time. It is important to thread resource-intensive code where sensible or you will be limited to just one of the user's cores, which as time goes on will be less and less of their system's true capacity.

As well as improving performance, threading can be used simply to process asynchronously. This is good when you have a job that the main thread can continue without knowing the result of (updating the UI, playing incidental music) or that could take a very long time (downloading a file from the internet). If you just want to postpone code until the future however, use Think().

To learn about how Source handles threads under the hood, see Tom Leonard's GDC 2007 presentation.

Thread safety

Todo: Thread safety

Using the thread pool

The engine provides (system cores - 1) threads that you can throw jobs onto very easily.

Warning.pngWarning:Since each pooled thread will run its assigned job to completion before moving to the next, and since many important jobs are thrown into the pool, you should not use these methods to perform operations that will take an indefinite amount of time (e.g. downloading a file). Create entirely new threads for such tasks.

ParallelProcess()

void MyThreadedFunc(int& data)
{
	Msg("Job %i started\n",data);
	ThreadSleep(1000);
	Msg("\tJob %i complete\n",data);
}

CUtlVector<int> jobvec;
for (int i=0;i<10;i++)
	jobvec.AddToTail(i);
ParallelProcess(jobvec.Base(),jobvec.Count(),&MyThreadedFunc);
// current thread waits for completion
Msg("All jobs complete\n");

This function takes a CUtlVector containing the data you want to process, and the function that does the processing, and splits the ensuing work optimally across the thread pool and the current thread. The current thread therefore does not continue until all parallel processing is complete.

Tip.pngTip:ParallelProcess() is templated. ITEM_TYPE and OBJECT_TYPE, below, are defined by what you provide.
ITEM_TYPE* pItems
Pass MyUtlVector.Base()
int items
Pass MyUtlVector.Count()
OBJECT_TYPE* pObject
Optional. If pfnProcess needs to access members of a specific object, pass it here.
void* pfnProcess(ITEM_TYPE&)
A pointer to a void function that accepts a reference to ITEM_TYPE.
Note.pngNote:If the argument is a pointer, that means your function must accept ITEM_TYPE*&!
void* pfnBegin()
void* pfnEnd()
Optional. Functions to call before and after each thread completes its assigned jobs.
int nMaxParallel
Optional. The maximum number of threads to split the job across.

ThreadExecute()

void test(int& data)
{
	Msg("thread %i started\n",data);
	ThreadSleep(1000);
	Msg("\tthread %i woke\n",data);
}

for (int i=0;i<10;i++)
	ThreadExecute(test,i);

Each call to ThreadExecute() queues up a new job for the thread pool to process (it's actually a macro for g_pThreadPool->QueueCall()). The current thread is not used, and so execution continues immediately.

ThreadExecute() returns a CJob object, which can be used to control the job's execution.

OBJECT_TYPE* pObject
Optional. If pfnProxied needs to access members of a specific object, pass it here.
void* pfnProxied([ARG_TYPE_1 ... ARG_TYPE_14])
The function to execute. It can have up to 14 arguments of any type.
ARG_TYPE_1 to ARG_TYPE_14
Optional. The arguments to pass to the function. Must match the number of arguments it accepts.

g_pThreadPool->AddCall()

Identical to ThreadExecute()/g_pThreadPool->QueueCall(), except that the job is added to the front of the queue instead of the back.

Creating new threads

Simple threads

Simple threads run a function asynchronously: the engine will set it running, then continue without waiting for it to complete. Please note that threads are expensive resources. If you can reuse a thread for a common task, then set up an event loop and send messages to it, instead of creating a new thread for each task of the same type.

struct MyThreadParams_t
{
	int number;
	char letter;
};

unsigned MyThread( void *params )
{
	MyThreadParams_t* vars = (MyThreadParams_t*) params; // always use a struct!

	// do some stuff

	Msg("Cannot print to console from this threaded function\n");

	// clean up the memory
	delete vars;

	return 0;
}

void SomeFunc()
{
	MyThreadParams_t* vars = new MyThreadParams_t;
	vars->number = 5;
	CreateSimpleThread( MyThread, vars );
}
Note.pngNote:CreateSimpleThread() expects a static function. If you are trying to call a member function of a class, you must type MyClass::MyThread to make the reference static. Include a pointer in the params if you need the thread to access its invoking object.
Note.pngNote:Do not use the stack to store memory on when sending parameters to another thread, as the values only exist while you are inside the scope in the function. Instead, use dynamic memory allocation or global variables!

Worker threads

Each CWorkerThread contains a number of variables, a calling command, and the mainloop int Run(). An instance of the class is declared inside a source file. The other threads can call this thread by calling the proper functions in this instance. The thread is not necessarily running when these functions are executed and will most likely be required to be started prior to any calls. A sample thread with a single function would look like this.

Warning.pngWarning:All code in this article is only tested for the client. A radically different approach might be used by the server!
//-----------------------------------------------------------------------------
// A thread that can execute a function asynchronously.
//-----------------------------------------------------------------------------
class CMyAsyncThread: public CWorkerThread 
{
public:
	CMyAsyncThread() :
	m_Parameter1( NULL ) ,
	m_Parameter2( NULL )
	{
		SetName( "MyAsyncThread" );
	}

	~CMyAsyncThread()
	{
	}

	enum
	{
		CALL_FUNC,
		EXIT,
	};

	bool	CallThreadFunction( char* Parameter1, char* Parameter2 )
	{
		Assert( !Parameter1);
		Assert( !Parameter2 );
		m_Parameter1 = Parameter1;
		m_Parameter2 = Parameter2;
		CallWorker( CALL_FUNC );

		return true;
	}

	int Run()
	{
		unsigned nCall;
		while ( WaitForCall( &nCall ) )
		{
			if ( nCall == EXIT )
			{
				Reply( 1 );
				break;
			}

			// Reset some variables
			char* Parameter1 = m_Parameter1;
			char* Parameter2 = m_Parameter2;
			m_Parameter1 = 0;
			m_Parameter2 = 0;

			Reply( 1 );
			FunctionToBeRunFromInsideTheThread(Parameter1,Parameter2);

		}
		return 0;
	}

private:
	char* m_Parameter1;
	char* m_Parameter2;
};

static CMyAsyncThread g_CMyAsyncThread;

This sample will now be explained in details. However parts of the following might not be confirmed and is based on guesses and experience.

A thread is declared as a child class of CWorkerThread. The constructor sets the name of the thread. The official Valve Threads the name of the class without the leading C, but in theory this value could be anything. In the constructor all parameters should be initalized to zero, avoiding any nasty situations where they could by accident be initalized to something else. The destructor is defined and should be used to clean up, but with thread safety in mind.

class CMyAsyncThread: public CWorkerThread 
{
public:
	CMyAsyncThread() :
	m_Parameter1( NULL ) ,
	m_Parameter2( NULL )
	{
		SetName( "MyAsyncThread" );
	}

	~CMyAsyncThread()
	{
	}

Like most programs written for Win32, the thread waits for messages before executing code. For simplicity these messages could be written from within a enum. The values of these constants do not matter, as long they're not equal. It is recommended to have an EXIT command in case you wish to shut down the thread, which is nessesary to avoid memory leaks, when the game closes.

enum
{
	CALL_FUNC,
	EXIT,
};

When you wish to run a certain function in a thread, you can send a message to the thread's main loop. You can in addition copy memory to variables declared within the class, which apparently seems to be shared with the thread, or transmitted. The details of this remains unknown but experience shows that all memory copied to these variables is available when running from within the custom thread. Valve tests the pointers sent to the threads, so we left the testing code here. The parameters sent to the function is then copied into the shared memory and sent to the thread prior to execution, but as previously stated, the details are unknown.

bool	CallThreadFunction( char* Parameter1, char* Parameter2 )
{
	Assert( !Parameter1);
	Assert( !Parameter2 );
	m_Parameter1 = Parameter1;
	m_Parameter2 = Parameter2;
	CallWorker( CALL_FUNC );

	return true;
}

When the thread is started, the mainloop Run() is run. Like Win32 applications it patiently wait for messages to arrive and do not use any CPU when waiting. When another thread sends a message to the thread, the thread will begin executing again. The message sent to the thread can be read from the nCall integer sent to WaitForCall(). In this sample we check if it's the EXIT code and breaks the main loop if it is. Please note that we do not check whether the actual message sent were CALL_FUNC since we only have one function. A proper thread with multiple functions should do this.

We read the parameters sent along from the class's memory and afterwards we reset them to NULL. This was originally used by Valve to avoid running the function two times at the same time, where ASSERTs would detect if the variables were set. It is easily possible to call other functions outside the class from within the main loop, which we will demonstrate later. The Reply() command sends back a reply, but how this value is retrieved elsewhere is unknown.

int Run()
{
	unsigned nCall;
	while ( WaitForCall( &nCall ) )
	{
		if ( nCall == EXIT )
		{
			Reply( 1 );
			break;
		}

		// Reset some variables
		char* Parameter1 = m_Parameter1;
		char* Parameter2 = m_Parameter2;
		m_Parameter1 = 0;
		m_Parameter2 = 0;

		Reply( 1 );
		FunctionToBeRunFromInsideTheThread(Parameter1,Parameter2);

	}
	return 0;
}

It is possible to share memory between the creating thread and the calling thread, and perhaps others as well. This is done by adding variables to the private section of the thread class. The engine automatically takes care of the copying and no further declarations is required.

private:
	char* m_Parameter1;
	char* m_Parameter2;
};

Most importantly, when the thread class is defined an instance of the thread must be created. Simply write the following line of code just after the class definition.

static CMyAsyncThread g_CMyAsyncThread;

Calling worker threads

With the interface set, all that is left to do is declare functions that call the thread and functions that are called by the thread. Threads can be for instance called this way.

Note.pngNote:Make a copy of all data referred to by a pointer instead of passing the pointer. The data within the pointer is subject to change, or be deleted, during execution of the thread. This is basic Thread Safety.
// Creates the thread and calls it
bool	CreateAThreadAndCallTheFunction( char* Parameter1, char* Parameter2 )
{		
	if ( !g_CMyAsyncThread.IsAlive() )
		g_CMyAsyncThread.Start();

	if ( !g_CMyAsyncThread.IsAlive() )
		Warning("CreateAThreadAndCallTheFunction() failed to start the thread!\n");

	// Thread safety: make some local copies! The real ones could be deleted/changed while we execute.
	char* NewParameter1 = FUNCTION_THAT_CREATES_A_COPY_OF_THE_MEMORY(Parameter1);
	char* NewParameter2 = FUNCTION_THAT_CREATES_A_COPY_OF_THE_MEMORY(Parameter1);
	
	g_CMyAsyncThread.CallThreadFunction( NewParameter1, NewParameter2);

	g_CMyAsyncThread.CallWorker( CMyAsyncThread::EXIT );
	return true;
}

Lastly we will define a function that is called by the thread.

// This function is run from within the thread
bool FunctionToBeRunFromInsideTheThread( char* Parameter1 , char* Parameter2 )
{
	Msg("The thread works. The parameters are %s, %s",Parameter1,Parameter2);
	return true;
}

The function that should be run from within the thread (FunctionToBeRunFromInsideTheThread()) can be requested executed by other threads by calling CreateAThreadAndCallTheFunction(), which will create a new instance of the thread, execute the function, and delete the new thread. If you wish to learn more about how threads work in Source, try look for different implementations made by Valve, which is available within the latest Source SDK Code.