Developer Diary: XNA Threading - The Problems
As an extension to my previous Developer Diary I thought it made sense to give some more background information on threading in .NET and how it may apply to XNA development. In part this is intended to provide some insight into the difficulties of threaded development. This will be a three post backgrounder.
This first post will introduce the example and discuss the problems with it. This example will be the point of reference for all three posts.
The Example
The premise of the example is that you want to put your enemy AI code in a separate thread so that Update and Draw will have as much time as possible to execute. The below example code is derived from a Microsoft example on how to draw models. I have modified the example to do AI calculations in a separate thread (there is no real AI code, just filler).
using System;
using System.Collections.Generic;
using System.Threading;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Storage;
namespace SampleThreading {
public class ThreadedEnemyComponent : DrawableGameComponent {
// general members
private ContentManager content;
Vector3 cameraPosition = new Vector3( 0.0f, 50.0f, -5000.0f );
float aspectRatio = 640.0f / 480.0f;
// thread members
private Thread enemyAIThread = null;
private bool isStopping = false;
// enemy specific data (model, velocity, position rotation, etc).
private Model enemyModel;
private Vector3 enemyVelocity = Vector3.Zero;
Vector3 enemyPosition = Vector3.Zero;
float enemyRotation = 0.0f;
/// <summary>
/// Basic constructor taking the game the component is to be a part of.
/// </summary>
/// <param name="theGame">The game the component will belong to.</param>
public ThreadedEnemyComponent( Game theGame )
: base( theGame ) {
content = new ContentManager( this.Game.Services );
}
/// <summary>
/// Override Initialize to start our separate AI thread.
/// </summary>
public override void Initialize( ) {
base.Initialize( );
enemyAIThread = new Thread( EnemyAIThreadMethod );
enemyAIThread.Start( );
}
/// <summary>
/// Override dispose to ensure our thread is shutdown.
/// </summary>
/// <param name="disposing">
/// Set to true to release manage and unmanaged resources.
/// Set to false to release unmanaged resources.
/// </param>
protected override void Dispose( bool disposing ) {
try {
isStopping = true;
if( disposing ) {
// let's shutdown our thread if it hasn't
// shutdown already
if( enemyAIThread != null ) {
enemyAIThread.Join( ); // wait for the to shutdown
enemyAIThread = null;
}
}
} finally {
base.Dispose( disposing );
}
}
protected override void LoadGraphicsContent( bool loadAllContent ) {
if( loadAllContent ) {
// load our enemy model
enemyModel = this.content.Load<Model>( @"Content\Models\
enemyModel" );
}
}
protected override void UnloadGraphicsContent( bool unloadAllContent ) {
if( unloadAllContent == true ) {
content.Unload( );
}
}
/// <summary>
/// The thread that runs the enemy AI
/// </summary>
private void EnemyAIThreadMethod( ) {
while( !isStopping ) {
// NOTE: this is just filler code.
// Real code would analyze user location and if multiple
// enemies perform flocking, or other behaviours.
enemyRotation -= 0.01f;
enemyVelocity += new Vector3( 1, 0, 1 );
enemyPosition += enemyVelocity;
// sleeping will probably be in order.
Thread.Sleep( 10 );
}
}
/// <summary>
/// The thread that does the drawing from frames
/// </summary>
public override void Draw( GameTime gameTime ) {
// ... draw other aspects of the scene....
// Copy any parent transforms.
Matrix[ ] transforms = new Matrix[ enemyModel.Bones.Count ];
enemyModel.CopyAbsoluteBoneTransformsTo( transforms );
// draw the enemy model
foreach( ModelMesh mesh in enemyModel.Meshes ) {
foreach( BasicEffect effect in mesh.Effects ) {
effect.EnableDefaultLighting( );
// ... but ensure we draw it at the right location
effect.World = transforms[ mesh.ParentBone.Index ]
* Matrix.CreateRotationY( enemyRotation )
* Matrix.CreateTranslation( enemyPosition );
effect.View = Matrix.CreateLookAt(
cameraPosition,
Vector3.Zero,
Vector3.Up );
effect.Projection = Matrix.CreatePerspectiveFieldOfView(
MathHelper.ToRadians( 45.0f ),
aspectRatio,
1.0f,
10000.0f );
}
mesh.Draw( );
}
// ... draw other aspects of the scene....
base.Draw( gameTime );
}
}
}
The Problems with the Sample
The thread method EnemyAIThreadMethod does a slew of calculations while the Draw method uses the results of the calculations to display the model. If you didn't know otherwise the above code looks perfectly fine.
In fact when you run it, more often than not it will behave as you would expect. However, it won't always be right and I don't mean per running of the application. On a frame-by-frame basis you may see things behave in an unexpected fashion, such as an odd warping of the model. There are two main reasons for this.
Uncontrolled Access to Data
Since there are two separate threads using the data, in this case one reading (Draw) and one writing (EnemyAIThreadMethod), it is possible that the Draw thread will use values for enemyPosition, enemyRotation, etc, while they are changing. In other words, in a single frame for a single model some of the meshes in the model could use old values while other meshes for the same model could use newer values. This will result in some rather interesting looking (as in bad) models.
.NET Memory and Synchronization Model
Another reason for odd behaviour is related to how .NET works. .NET is essentially defined such that if nothing in a method or in a member declaration indicates there are multiple threads then .NET assumes it is single threaded code. The simple declaring and starting of a thread, as done in the above sample, isn't sufficient. Your code needs to indicate its awareness of threads.
Why? .NET attempts to optimize the code through a variety of techniques. A common optimization is to cache data in processor or system caches. For example, enemyRotation could be cached separately in the EnemyAIThreadMethod thread and the Draw thread. This means that when you change enemyRotation in the EnemyAIThreadMethod thread the Draw thread may not see the change until something triggers the caches to flush back to main memory. The reason for caching is speed; accessing a cache is much faster than accessing main memory.
Follow-on Posts
This post was just meant to introduce the typical problems encountered with threading. The follow-on posts, which I'll post over the next couple days, will cover ways to correct the problems outlined here.
- Discussing the Problem
- What are Locks?
- Other options (coming soon)
Comments