Developer Diary: XNA Threading - Locks
In my previous Developer Diary I discussed the problems that can arise from threaded development particularly as outlined in the example. In this post I discuss locks, what they are and how they can fix the problems with the example.
What are Locks?
The lock is one of the primary synchronization primitives in .NET. Locks are largely used to control access to a particular part of code (commonly called a critical section).
When a thread owns a lock no other thread is able to use the lock and execute the code protected by the lock. This results in the other threads waiting for the first thread to release its ownership. Once the first thread releases ownership one of the waiting threads will then own the lock and continue executing.
The basic way to create and use a lock is as follows:
class SomeClass {
private object syncLock = new object( ); // create the lock in the class
public void SomeMethod( ) {
lock( syncLock ) { // request ownership of the lock
// protected code here
} // release ownership of lock
}
}
The variable syncLock can actually be of any reference type. This means primitive and value types (such as , int, floatVector3, etc) cannot be used.
Updating Our Example
So how do locks help given our example? First let's update the code from the example in the previous Developer Diary (Note: the fully updated source is at the end of this post).
First we need our class-level shared lock.
private object enemyDataLock = new object( );
After that we need to update the code in EnemyAIThreadMethod to lock just the area that updates the data that is used in the Draw method. You want to lock as little as possible otherwise you may slow down other threads who are also trying to use the lock. For example, if the Thread.Sleep( 10 ); statement was in the lock block then we would make it more difficult for the Draw method to maintain a high frame rate.
private void EnemyAIThreadMethod( ) {
while( !isStopping ) {
// lock to make sure data is updated
// correctly and Draw doesn't get
// partial results
lock( enemyDataLock ) {
// 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, but outside
// of the lock so we have Draw a chance
Thread.Sleep( 10 );
}
}
Finally we update the Draw method. At first glance you may consider simply putting a lock around the entire foreach block to ensure that we use the same values for enemyRotation and enemyPosition for each mesh in the model. The Draw method would look like this:
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 );
// make sure we have consistent data
lock( enemyDataLock ) {
// 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 );
}
That is a lot of code to lock. Remember you want to lock as little as possible. A better approach is to create local copies of the data and only lock the point where the data is copied. Our new Draw method looks as follows:
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 );
// local copies of the class data
Vector3 enemyPositionCopy;
float enemyRotationCopy;
// make sure we have consistent data
lock( enemyDataLock ) {
enemyPositionCopy = enemyPosition;
enemyRotationCopy = enemyRotation;
}
// 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( enemyRotationCopy )
* Matrix.CreateTranslation( enemyPositionCopy );
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 );
}
}
This seriously cuts down on the amount of code locked allowing both threads to run more quickly. As a note, the Vector3 type is a value type (struct), instead of a reference type (class). This means the line enemyPositionCopy = enemyPosition; isn't simply assigning a reference, but making a field by field copy of the Vector3 members. This is an important distinction. If we had reference types then other code would need to be considered. I'll cover that in my next article.
Why Does It Work?
I mentioned in the previous article that our example needed updating to cover two problems.
The first problem was uncontrolled access to the data. This is fixed by the lock statements. The use of a lock prevents the Draw thread from using the enemy data while the data is being updated in the EnemyAIMethod thread and conversely the EnemyAIMethod can't update the values while the Draw method is copying them.
The second problem was getting around .NET's memory and synchronization model. The reason this isn't an issue is due to how locks work. I'll attempt a simple explanation since it is much more complicated than I'm describing. Locks explicitly indicate to .NET that there is threaded code so that certain optimizations will not occur. Some optimizations, such as the caching of data in processor and system caches, aren't an issue because a lock ensures that modified data makes it in and out of main memory.
As you may have guessed using locks incurs a performance penalty. However, it can be a small price to pay for correctness. More advanced options are available, but they are harder to code for. I'll outline one such approach in my next post.
In Summary
Locks are an important part of threading in .NET but, as with all things, they have their advantages and disadvantages.
Advantages of locks:
- Easier to code and understand
- Handles both the Uncontrolled Access to Data and .NET Memory and Synchronization Models problems
Disadvantages of locks:
- Is fairly expensive to execute
- Easier to create deadlock situations
What is a Deadlock?
It is best to discuss deadlocks when you have an example. Fortunately the XNA example code we have been using isn't subject to deadlocks, but the following piece of code is:
private object syncLockA = new object( );
private object syncLockB = new object( );
// thread one is running here
private void ThreadMethodOne( ) {
while( !isStopping ) {
lock( syncLockA ) {
// do some work
lock( syncLockB ) {
// do some more work
}
}
}
}
// thread two is running here
private void ThreadMethodTwo( ) {
while( !isStopping ) {
lock( syncLockB ) {
// do some work
lock( syncLockA ) {
// do some more work
}
}
}
}
The potential for deadlocks occur when two or more threads have two or more locks they are sharing. In the above code a deadlock can occur if we have a thread executing in ThreadMethodOne that is currently trying to acquire syncLockB after having acquired syncLockA, and we have another thread executing in ThreadMethodTwo trying to acquire syncLockA after having acquired syncLockB. These threads are now stuck forever since they are waiting for the other thread to release the additional lock needed.
That's a deadlock. Once you have multiple threads and multiple locks you need to be extra careful with your code.
Follow-on Posts
I hope this post gave you a good overview of how to use locks in .NET. In my next post I'll talk about another mechanism that can be used to handle our example, as well as some pointers for additional .NET threading details and recommendations.
- Discussing the Problem
- What are Locks?
- Other options (coming soon)
Complete Updated Example Source Code
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 volatile bool isStopping = false;
private object enemyDataLock = new object( );
// enemy specific data (model, velocity, position rotation, etc).
private Model enemyModel;
private Vector3 enemyVelocity = Vector3.Zero;
private Vector3 enemyPosition = Vector3.Zero;
private 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 ) {
// lock to make sure data is updated
// correctly and Draw doesn't get
// partial results
lock( enemyDataLock ) {
// 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, but outside
// of the lock so we have Draw a chance
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 );
// local copies of the class data
Vector3 enemyPositionCopy;
float enemyRotationCopy;
// make sure we have consistent data
lock( enemyDataLock ) {
enemyPositionCopy = enemyPosition;
enemyRotationCopy = enemyRotation;
}
// 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( enemyRotationCopy )
* Matrix.CreateTranslation( enemyPositionCopy );
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 );
}
}
}
Nice series of articles ... http://amapplease.blogspot.com/2007/04/xna-threading.html
... so said 'Ultrahead' on 04/18/2007 @ 11:28:38 PM [Direct Link]
Thanks Ultrahead.
... so said 'Joseph Molnar' on 04/19/2007 @ 08:57:14 AM [Direct Link]
Funny URL blogs.spouting-tech.com Proud to tell you, the URL free for allfresh website. Make a note Free classifieds Ads Board - http://www.classifieds-market.net is there for free. Will be glad to tell you Web marketing specialists, are any here? Get more targeted traffic on my web site When I ordered web site design, I choose http://www.avazo.com I expected your suggestions Find dream job with FREE employment service on http://www.m
... so said 'assonidoste' on 04/04/2009 @ 04:03:22 PM [Direct Link]
Hi Guys How you steal such domain blogs.spouting-tech.com My respect for your project Be thanksful we suggest this link as interesting Free online magazine, new style media - http://www.avazo.com
... so said 'wainiciarry' on 04/18/2009 @ 05:40:58 AM [Direct Link]