Written by: Wednesday, March 23, 2011
The .NET Framework 4 contains the Task Parallel Library (TPL). It introduces the concept of tasks. Tasks represent asynchronous operations that can be executed concurrently. Developers no longer have to manage threads or thread pool work items directly. The library provides a higher-level API for writing multithreaded application or games. The only negative thing we can say about the TPL is that it is not supported on the .NET Compact Framework and is therefore not available on the Xbox 360 (or the WP7).
When we added multithreading support for our libraries1, we looked for an easy-to-use, cross-platform threading library. After evaluating a lot of solutions we found the ParallelTasks library on CodePlex. The library is a lightweight replacement for the TPL. It offers all relevant features such as
Kudos to Aphid who has done a great job!
Since the library is open source (Ms-PL) we have taken the freedom to make a few changes and improvements2 and integrate the library into the DigitalRune Helper Library.
For more information see the documentation: DigitalRune.Threading.Parallel.
In the following I want to show a few examples where task-based multithreading has helped to boost the performance of our libraries.
A CollisionDomain in DigitalRune Geometry manages a set of collision objects. In Update() the CollisionDomain determines all collisions between those objects. The method consists of two parts: The “broad phase” and the “narrow phase”. The broad phase yields pairs of objects. It returns all pairs of objects where the axis-aligned bounding boxes overlap. This is necessary to quickly sort out objects that don’t collide.
The narrow phase is the computationally expensive part. It iterates over all pairs of objects and determines whether the objects collide. It computes the penetration depth, the normal vector and other properties of a contact point. The method basically looks like this:
private void NarrowPhase(float deltaTime) { // Notes: // - CandidatePairs is the collection of pairs returned by the broad phase. // - ContactSets is the resulting collection of colliding objects. ContactSets.Clear(); foreach (var pair in CandidatePairs) { // Fetch the appropriate collision algorithm and compute the contacts // between the pair of objects. CollisionDetection.AlgorithmMatrix[pair].UpdateContacts(pair, deltaTime); // 'pair' now contains all required information: Whether the objects collide // and where the objects intersect. // If we have found a collision, add it to the results. if (pair.HaveContact) { ContactSets.Add(pair); } } }
Now, to speed things up we can perform the computations of all collisions in parallel. The necessary changes are quite simple:
private void NarrowPhase(float deltaTime) { // Note: // - CandidatePairs is the collection of pairs returned by the broad phase. // - ContactSets is the resulting collection of pairs of colliding objects. ContactSets.Clear(); Parallel.ForEach(CandidatePairs, pair => { // Fetch the appropriate collision algorithm and compute the contacts // between the pair of objects. CollisionDetection.AlgorithmMatrix[pair].UpdateContacts(pair, deltaTime); }); foreach (var pair in CandidatePairs) { // If we have found a collision, add it to the results. if (pair.HaveContact) { ContactSets.Add(pair); } } }
We split the foreach loop in two parts: A parallel loop that computes the collisions and a sequential loop that adds the results to a collection. The sequential loop is necessary because the ContactSets collection is not thread-safe.
To be honest, the actual code in DigitalRune Geometry is more complex - I have greatly simplified the code. But the required steps to parallelize the code are the same. This examples shows how easy it is to execute code in parallel using the Parallel class. As a developer you no longer have to manage threads or jobs directly.
Now, the really difficult part is that data structures and algorithms in the parallel loop need to be thread-safe. In our case we had to ensure the following:
The central object in DigitalRune Physics is the Simulation. It manages all simulated bodies, forces, constraints, etc. The simulation needs to be updated by calling Update(). The method checks how much time has passed and progresses the simulation by a number of fixed sized steps. One such step is called a sub time step.
Here is what the multithreaded version of a sub time step looks like:
private void SubTimeStep(float fixedTimeStep) { // Run the collision detection. UpdateContacts(); // Compute forces and torques. EvaluateForces(); // Update velocities. Parallel.For(0, RigidBodies.Count, UpdateVelocity); // Find simulation islands. IslandManager.Update(); // Update simulation islands. Parallel.For(0, IslandManager.Islands.Count, SolveIsland); // Update positions and orientations. Parallel.For(0, RigidBodies.Count, UpdatePose); // CCD motion clamping. DoCcdMotionClamping(); // Advance simulation time. Time += TimeSpan.FromSeconds(fixedTimeStep); // Raise SubTimeStepFinished event. OnSubTimeStepFinished(EventArgs.Empty); }
The first method updates the internal collision domain which runs on multiple threads as shown above. Then there are several for-loops that we have parallelized using Parallel. The methods called in the for-loops (e.g. UpdateVelocity) are simple delegates of type Action<int>. The method receives the loop index and performs the required action on the object with the given index.
Again, I have omitted some minor details. But in this case the actual code in DigitalRune Physics looks pretty much as the code above. Pretty simple, right? There is no hidden magic here. (Well, there is quite a lot of magic involved, but it is all neatly wrapped in the Parallel class.)
The DigitalRune Helper Library, which is is part of all our gaming libraries, provides a solution for task-based multithreading. To speed up our libraries on multi-core systems we had to identify tasks that can run in parallel and then distribute them across multiple threads.
Writing a multithreaded game in XNA can be pretty easy using the right tools. In our case “task-based multithreading” was a perfect fit.
By the way, it is also quite simple to run the physics code as a task parallel to other game services, like graphics, AI, input, networking, etc. – so that all your CPU cores get their share of the work. This is something we will write more about in future blog posts.
1 DigitalRune Geometry supports multithreading since version 1.3 and DigitalRune Physics since version 1.1
2 We fixed some issues with contention in the task scheduler and made a few optimizations for PC and Xbox 360.
5 comment(s) so far...
Re: DigitalRune Helper Library: Multithreading in XNA Why did you take an open source package and include it as it it was yours by changing namespaces etc? Why not just ship the compiled binary of the Open source package so that people can see its origins?Did you roll your improvements back into the Open Source package? (or are you going to?)(Yes I know MS-PL allows for this, it just always feels wrong when Open Source is used in this way especially when its shipped with a commercial product)
Re: DigitalRune Helper Library: Multithreading in XNA
Why did you take an open source package and include it as it it was yours by changing namespaces etc? Why not just ship the compiled binary of the Open source package so that people can see its origins?Did you roll your improvements back into the Open Source package? (or are you going to?)(Yes I know MS-PL allows for this, it just always feels wrong when Open Source is used in this way especially when its shipped with a commercial product)
Re: DigitalRune Helper Library: Multithreading in XNA Hi Zman,thanks for mentioning your concerns. Part of the intention of this blog post was to give credit to the original ParallelTasks project (as we also did in our documentation and, of cource, in the source code). So why didn't we link our libraries against the original binary? Let me assure you that this wasn't an easy decision. The main reasons were control and flexibility. We changed the namespace because we made changes to the API. We wanted the API to be more similar to the .NET TPL. These were no functional changes, just a matter of personal style. Also, we removed code that duplicated functions we already had and could cause confusion.We made some minor changes to the task scheduling in the library. (As described in the blog post our library uses lots of parallel loops - often nested parallel loops.) Our changes improve the performance of our libraries. But I am not sure whether all changes should be merged back into the original project.We also didn't want to add an external dependency. (The threading classes are only a minor part of our game programming codebase - 15 C# files out of >1000 files, or about 1.2% of the lines of code. An additional external dependency would require additional documentation and maintenance effort in a disproportionate amount.)And I have personally made some bad experiences using other open source projects. Contributors were trying to fix code, but instead introduced new problems or breaking changes. At some point we had to create our own fork to ship a product. We didn't want this to happen again for such a small but critical piece of code. (The task scheduler is a very delicate piece of code. Small changes can have huge performance impacts or sometimes hidden consequences. It took us excessive testing until we were confident enough to ship our version.)So, should we ship it using the original name, but with inofficial changes? What if we have introduced new bugs? This could also hurt the reputation of the original work.We want to give the users of our libraries the benefit of a tightly integrated and fully supported threading solution. It is not our intention to financially exploit the work of the original author. And we plan to share our changes with the original project.Thanks again for your feedback!
Hi Zman,thanks for mentioning your concerns. Part of the intention of this blog post was to give credit to the original ParallelTasks project (as we also did in our documentation and, of cource, in the source code). So why didn't we link our libraries against the original binary? Let me assure you that this wasn't an easy decision. The main reasons were control and flexibility. We changed the namespace because we made changes to the API. We wanted the API to be more similar to the .NET TPL. These were no functional changes, just a matter of personal style. Also, we removed code that duplicated functions we already had and could cause confusion.We made some minor changes to the task scheduling in the library. (As described in the blog post our library uses lots of parallel loops - often nested parallel loops.) Our changes improve the performance of our libraries. But I am not sure whether all changes should be merged back into the original project.We also didn't want to add an external dependency. (The threading classes are only a minor part of our game programming codebase - 15 C# files out of >1000 files, or about 1.2% of the lines of code. An additional external dependency would require additional documentation and maintenance effort in a disproportionate amount.)And I have personally made some bad experiences using other open source projects. Contributors were trying to fix code, but instead introduced new problems or breaking changes. At some point we had to create our own fork to ship a product. We didn't want this to happen again for such a small but critical piece of code. (The task scheduler is a very delicate piece of code. Small changes can have huge performance impacts or sometimes hidden consequences. It took us excessive testing until we were confident enough to ship our version.)So, should we ship it using the original name, but with inofficial changes? What if we have introduced new bugs? This could also hurt the reputation of the original work.We want to give the users of our libraries the benefit of a tightly integrated and fully supported threading solution. It is not our intention to financially exploit the work of the original author. And we plan to share our changes with the original project.Thanks again for your feedback!
Re: DigitalRune Helper Library: Multithreading in XNA If you offer up your changes back as a patch so that the original author at least has the opportunity to incorporate them then I think you have 'worked off' your debt. Whenever I use Open Source I always try to contribute back (or make a sizeable donation of course)
If you offer up your changes back as a patch so that the original author at least has the opportunity to incorporate them then I think you have 'worked off' your debt. Whenever I use Open Source I always try to contribute back (or make a sizeable donation of course)
Re: DigitalRune Helper Library: Multithreading in XNA They saved you a lot of time and effort by the sounds of it and you made some core performance improvements by the sounds of it to the task system...You should at least provide a patch for them with your improvment, isn't that the whole point of open source software?Also this could benifit you as then on there site they would say "Thanks to digital rune we have some new performance enhancments to the task system, you should go check them out." or something along those lines.In short you should put up a patch or something to give back to the community... You will benifit from it in return :)
They saved you a lot of time and effort by the sounds of it and you made some core performance improvements by the sounds of it to the task system...You should at least provide a patch for them with your improvment, isn't that the whole point of open source software?Also this could benifit you as then on there site they would say "Thanks to digital rune we have some new performance enhancments to the task system, you should go check them out." or something along those lines.In short you should put up a patch or something to give back to the community... You will benifit from it in return :)
Re: DigitalRune Helper Library: Multithreading in XNA Hi Daniel, I agree. As Martin commented above, we will share our changes and will not keep them secret.
Hi Daniel, I agree. As Martin commented above, we will share our changes and will not keep them secret.
A collection of the most useful blog articles can be found here:
Article Collection (on Documentation page)
DigitalRune is a trademark of Garstenauer Information Technology OG.
Garstenauer Information Technology OG Weingartenstrasse 35, 4452 Ternberg Austria (EUROPE) office@digitalrune.com