Article : Multi-threading your XNA
2. A Look at Multi-Threading Primitives
This section will not be a complete guide to threading in C#. For that, you can access a free ebook with that name: Threading in C#. But I also can't simply give you that link and go on with the article, so I'll cover the primitives I'll be using throughout the article.
In C#, the Thread class creates and controls a thread, sets its priority and reads its status. When creating an instance of the Thread class, we need to pass a ThreadStart or a ParameterizedThreadStart delegate to its constructor. A ThreadStart delegate represents a method with no arguments that will be run on the thread. For example, if we have a method a(), we can create a new Thread on which to run that method with the following code.
<br />
void a()<br />
{<br />
[...]<br />
}<br />
[...]</p>
<p> ThreadStart threadStart = new ThreadStart(a);<br />
Thread newThread = new Thread(threadStart);<br />
newThread.Start();<br />
or simply
<br />
Thread t = new Thread(new ThreadStart(a));<br />
t.Start();<br />
But simply starting functions on threads is not enough. Data that can be accessed by more than one thread at once needs to be protected. To do this, the lock statement can be used. The lock keyword marks a statement block as a critical section by obtaining the mutual-exclusion lock for a given object, executing a statement, and then releasing the lock. In other words, while a thread is in a section of code protected by a lock on an object, that no other thread may enter any area of code protected by the same lock.
<br />
lock(lockObject)<br />
{<br />
//this code, and all other code surrounded by a lock on lockObject can only be accessed by one thread at a time. All other threds will be blocked, until the thread finishes.<br />
}<br />
The last class I will shortly explain is the AutoResetEvent. This class allows threads to communicate between them using signals. In order to start waiting for a signal, a thread can call the WaitOne() function of an AutoResetEvent object. If this event has been signaled previously, the thread can resume execution. Othewise, the thread is blocked, and starts waiting until someone signals the AutoResetEvent, by calling the Set() function. The class is called AutoResetEvent, because when a thread that was waiting for a signal is released, the AutoResetEvent is automatically reset, and enters the non-signaled state.
The following code shows how to declare and initialize an AutoResetEvent.
<br />
AutoResetEvent myEvent = new AutoResetEvent(false); // you can specify the initial state through the constructor's parameter.<br />
[...]<br />
//when a thread tries to enter a protected region of code, it calls WaitOne()<br />
myEvent.WaitOne();<br />
[...]<br />
//the thread stays blocked until some other thread signals the event using<br />
myEvent.Set()<br />
These are the most important primitives we will use. For more details, there are lots of places where you can read about threading primitives, with the most important being MSDN and the free ebook I linked above.
November 28th, 2009 - 06:06
Hi Catalin, I originally went down a very similar research path as what you are describing in your threaded-rendering system.
However I did some prototyping and came to the realization that the GPU is already running asynchronously, and only forces a CPU-side block if the previous frame hasnt finished when the next .Begin() is called. This result plus the added complexity of caching game-state led me to abandon this system.
What we are planning to do on our engine is to use a seperate cpu thread to process vertex data (batch instancing), but that’s really no different than multithreading other cpu-side modules.
November 28th, 2009 - 07:41
Hi Jason.
Yes, the GPU does indeed run asynchronously from the CPU. However, the speed of the GPU is rarely the main performance bottleneck. Most time, the problem is with the communication between the CPU and the GPU which happens at each DrawXXX() call. Having a high number of draw calls eats up lots of CPU time, and that is what I am trying to reduce in this article. So what I am threading is not actually the GPU-drawing, but the CPU invoking of draw calls (which happens at driver-level).
In most multi-threading approaches, you either try to distribute the non-graphics CPU-work on multiple threads, or try to distribute the GPU-communication work done by the CPU.
But yes, the approach is not what I’d recommend for a truly advanced system. It is good for learning purposes, and it is definitely a robust way to add threading to your game and obtain a good performance boost, but at the cost of extra memory needed for the two buffers.
For a more complex threading system, there are some other ways to do it, as presented in some of the papers I linked to at the bottom of the article. Each method has it’s advantages, and you can’t always decide that one way is better then others. It usually depends on the structure of the rest of the engine.
November 28th, 2009 - 07:46
Nice one. It will help me a lot.
Thanks,
Timo
November 30th, 2009 - 03:12
yah, I think your system is a great intro to the very complex world of multithreading. Like I said, I originally went down a very similar path to what you are describing, so I do feel that there are benefits, but in my situation the drawbacks outweighed them.
October 9th, 2010 - 20:25
a simpler approach might be:
a) each GameComponent maintains two DrawState objects
b) Draw() method ONLY uses information in current DrawState object to draw
c) Update() keeps only ONE version of “UpdateState”
d) single point of control for flip flop using Game.CurrentDrawStateIndex = 0 or 1
e) GameComponent.Draw() { CurrentState = DrawState[Game.CurrentDrawStateIndex]; }
this is easy to design – anything that Draw() requires goes into DrawState
November 22nd, 2010 - 17:37
When I try using Multiple Threads, the controls don’t seem to work, however they do work if I use a single thread (uncomment the line in the draw method and comment out the one in load). Any idea why this is?
December 13th, 2010 - 05:41
I should notice something: because of the way xna handles input, Keyboard input can be received only from the main thread, due to this you have to create your own input handler to fix this