Difference between revisions of "Threads"

From Valve Developer Community
Jump to: navigation, search
(Final draft. Anyone, fell free to correct me if I'm wrong. :D)
Line 118: Line 118:
 
When the thread is started, the mainloop <code>Run()</code> 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 <code>nCall</code> integer sent to <code>WaitForCall()</code>. In this sample we check if it's the <code>EXIT</code> 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.
 
When the thread is started, the mainloop <code>Run()</code> 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 <code>nCall</code> integer sent to <code>WaitForCall()</code>. In this sample we check if it's the <code>EXIT</code> 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 <code>NULL</code>. This was originally used by Valve to avoid running the function two times at the same time, where <code>ASSERT</code>s 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.
+
We read the parameters sent along from the class's memory and afterwards we reset them to <code>NULL</code>. This was originally used by Valve to avoid running the function two times at the same time, where <code>ASSERT</code>s 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 <code>Reply()</code> command sends back a reply, but how this value is retrieved elsewhere is unknown.
  
 
<pre>
 
<pre>
Line 145: Line 145:
 
  }
 
  }
 
</pre>
 
</pre>
 +
 +
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.
  
 
<pre>
 
<pre>
// Connects to the content servers and executes a function there with a given parameter
+
private:
// Runs the code in this thread, so the game will hang until done. Avoid this by using PingBack() instead.
+
char* m_Parameter1;
bool PingBackThreaded( char* Function, char* Parameter )
+
char* m_Parameter2;
{
+
};
 +
</pre>
  
}
+
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.
  
//-----------------------------------------------------------------------------
+
<pre>
// A thread for async PingBack'ing.
+
static CMyAsyncThread g_CMyAsyncThread;
//-----------------------------------------------------------------------------
+
</pre>
class CAsyncPingBackThread : public CWorkerThread
 
{
 
public:
 
  CAsyncPingBackThread() :
 
  m_Function( NULL ) ,
 
  m_Parameter( NULL )
 
  {
 
  SetName( "AsyncPingBackThread" );
 
  }
 
 
 
  ~CAsyncPingBackThread()
 
  {
 
  }
 
  
  enum
+
== Calling threads in Source ==
  {
 
  CALL_FUNC,
 
  EXIT,
 
  };
 
  
  bool PingBack( char* Function, char* Parameter )
+
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.
  {
 
  Assert( !Function );
 
  Assert( !Parameter );
 
  m_Function = Function;
 
  m_Parameter = Parameter;
 
  CallWorker( CALL_FUNC );
 
  Assert( !Function );
 
  Assert( !Parameter );
 
  return true;
 
  }
 
  
  int Run()
+
{{note|Make a copy of all data refered 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.}}
  {
+
<pre>
  unsigned nCall;
+
// Creates the thread and calls it
  while ( WaitForCall( &nCall ) )
+
bool CreateAThreadAndCallTheFunction( char* Parameter1, char* Parameter2 )
  {
 
  if ( nCall == EXIT )
 
  {
 
  Reply( 1 );
 
  break;
 
  }
 
 
 
  if (!m_Function)
 
  {
 
  IngameError("MaxsiDistribution::CAsyncPingBackThread::Run() does not have m_Function set!");
 
  m_Function = 0;
 
  m_Parameter = 0;
 
  Reply( 0 );
 
  return 0;
 
  }
 
 
 
  if (!m_Parameter)
 
  {
 
  IngameError("MaxsiDistribution::CAsyncPingBackThread::Run() does not have m_Parameter set!");
 
  m_Function = 0;
 
  m_Parameter = 0;
 
  Reply( 0 );
 
  return 0;
 
  }
 
 
 
  // Reset some variables
 
  char* Function = m_Function;
 
  char* Parameter = m_Parameter;
 
  m_Function = 0;
 
  m_Parameter = 0;
 
 
 
  Reply( 1 );
 
  MaxsiDistribution::PingBackThreaded(Function,Parameter);
 
 
 
  // Clean up
 
  delete[] Function;
 
  delete[] Parameter;
 
  }
 
  return 0;
 
  }
 
 
 
private:
 
char* m_Function;
 
char* m_Parameter;
 
};
 
 
 
static CAsyncPingBackThread g_AsyncPingBackThread;
 
 
 
 
 
// Creates the thread, from there calls the pingback and then does fancy stuff.
 
bool PingBack( char* Function, char* Parameter )
 
 
{
 
{
if ( !g_AsyncPingBackThread.IsAlive() )
+
if ( !g_CMyAsyncThread.IsAlive() )
 
{
 
{
g_AsyncPingBackThread.Start();
+
g_CMyAsyncThread.Start();
 
}
 
}
if ( !g_AsyncPingBackThread.IsAlive() )
+
if ( !g_CMyAsyncThread.IsAlive() )
 
{
 
{
Warning(":PingBack() failed to start the async pingback thread!");
+
Warning("CreateAThreadAndCallTheFunction() failed to start the thread!\n");
 
}
 
}
  
// Make some local copies! (Because the real ones COULD get deleted or changed while we upload)
+
// Make some local copies! (Because the real ones COULD get deleted or changed while we execute (=Thread Safety!))
char* NewFunction = BuildString(1,Function);
+
char* NewParameter1 = FUNCTION_THAT_CREATES_A_COPY_OF_THE_MEMORY(Parameter1);
char* NewParameter = BuildString(1,Parameter);
+
char* NewParameter2 = FUNCTION_THAT_CREATES_A_COPY_OF_THE_MEMORY(Parameter1);
 
 
g_AsyncPingBackThread.PingBack ( NewFunction, NewParameter);
+
g_CMyAsyncThread.CallThreadFunction( NewParameter1, NewParameter2);
 +
 
 +
g_CMyAsyncThread.CallWorker( CMyAsyncThread::EXIT );
 +
return true;
 +
}
 +
</pre>
  
g_AsyncPingBackThread.CallWorker( CAsyncPingBackThread::EXIT );
+
Lastly we will define a function that is called by the thread.
 +
 
 +
<pre>
 +
// 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;
 
return true;
 
}
 
}
 
</pre>
 
</pre>
 +
 +
The function that should be run from within the thread (<code>FunctionToBeRunFromInsideTheThread()</code>) can be requested executed by other threads by calling <code>CreateAThreadAndCallTheFunction()</code>, 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 lastest Source SDK Code.
  
 
[[Category:Programming]]
 
[[Category:Programming]]

Revision as of 17:11, 11 December 2008

Modern computers utilize multiple processors and recently games have begun to take advantage from this. By splitting code into multiple threads, it's possible to run code simutaniously on multiple processors. Threads are a very important part of Windows, where most programs use bunches of threads. Please note that the number of threads available is not limited by the number of CPUs, but by the memory in windows. Each thread will by default have a stack size of one megabyte, therefore Win 32 has a limitation of 2048 threads. This article is not about threads in general, but about threads in Source, and assumes you already know how threads work. For more information on how threads work in Windows please read about it on another site.

Source utilizes threads by putting each kind of operation into its own thread. Source has for instance a thread for physics, a thread for AI, a thread for rendering, and so on. If the computer does not have multiple CPUs, it will run every thread on the same CPU and switch between them every few miliseconds. Programmers using the Source SDK can use threads as well by using Valve's classes, and can run important functions, with a lot of waiting, asynchronously. These functions could for instance be WinSock operations, disk read/writes or similar. Functions that just need to wait for a while and then run some code would be better off being implemented by using the SetThink functions.

Threads in Source

Threads in Source are child classes of CWorkerThread. These contain 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 nessesarily 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.

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

		  ~CAsyncPingBackThread()
		  {
		  }

		  enum
		  {
			  CALL_FUNC,
			  EXIT,
		  };

		  bool	CallThreadFunction( char* Parameter1, char* Parameter2 )
		  {
			  Assert( !Parameter1);
			  Assert( !Parameter2 );
			  m_Function = Function;
			  m_Parameter = Parameter;
			  CallWorker( CALL_FUNC );

			  return true;
		  }

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

				  // Reset some variables
				  char* Parameter1 = m_Parameter2;
				  char* Parameter2 = m_Parameter1;				
				  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:
		  CAsyncPingBackThread() :
		  m_Parameter1( NULL ) ,
		  m_Parameter2( NULL )
		  {
			  SetName( "MyAsyncThread" );
		  }

		  ~CAsyncPingBackThread()
		  {
		  }

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_Function = Function;
			  m_Parameter = Parameter;
			  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_Parameter2;
				  char* Parameter2 = m_Parameter1;				
				  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 threads in Source

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.png Note: Make a copy of all data refered 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");
		}

		// Make some local copies! (Because the real ones COULD get deleted or changed while we execute (=Thread Safety!))
		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 lastest Source SDK Code.