Using the Unity API from Other Threads
The Unity API can mostly only be used from the main thread. This is used as an excuse by Unity developers to write all their code on the main thread. This makes their code run 2-6x slower. So what can we do about it? Today’s article presents a simple way to use the Unity API from other threads. Read on to learn how to unlock all that extra CPU power!
There’s nothing we can do about Unity requiring us to access its API from only the main thread. Sure, there are exceptions to this rule such as using Vector3
from any thread, but generally we can’t do anything useful like access the hierarchy outside the main thread.
So what we need is a way for a non-main (“worker”) thread to tell the main thread to access the Unity API. Then we need the main thread to communicate its results back to the worker thread.
Doing this is actually pretty simple. It allows us to set up an easily-extensible pattern that we can use to support any Unity API functionality we want to use from other threads. It’s type-safe, thread-safe, and easy-to-use. Let’s break it down into parts.
First, the commands. These are messages from worker threads to the main thread telling the main thread what the worker thread wants to do. A command is simply a function call that puts an instance of a BaseCommand
class into a Queue<BaseCommand>
. Later on the main thread will go through this queue of commands and execute them.
Now for the other side: results. This is how the main thread tells a worker thread that it’s done processing one of its commands. A Result<T>
simply wraps up the result value of type T
and uses an AutoResetEvent
to make the worker thread sleep until the result value is ready.
Of course we don’t want this system to raise the ire of the garbage collector, or we’ll be giving back some of the performance benefits of using threads in the first place. So it’s important to talk about how we’re not going to need to create and release these BaseCommand
and Result
objects.
BaseCommand
objects are internally pooled by the MainThreadQueue
class that’s at the heart of this system. Worker threads don’t need to worry about it at all. In fact, the command class types are private
so they’re not even exposed outside the MainThreadQueue
class.
Result
objects are passed in by the worker thread. They can be reused after the command finishes. Exactly how to reuse them is up to the worker thread because only it knows when its done using the result.
With this in mind, let’s look at a little script that uses this system. The script’s job is to create a bunch of game objects and set their positions. It does this with four threads that all share a MainThreadQueue
object. As you read, notice how there’s no manual thread synchronization with locks or mutexes. It’s not quite as direct as using the Unity API normally, but it’s pretty straightforward:
using System.Threading; using UnityEngine; class TestScript : MonoBehaviour { class ThreadInfo { public int ThreadId; public MainThreadQueue MainThreadQueue; public int CreateCount; } MainThreadQueue mainThreadQueue; void Start() { // Create the queue to do work on the main thread mainThreadQueue = new MainThreadQueue(); // Start some threads var threadStart = new ParameterizedThreadStart(OtherThread); for (int i = 0; i < 4; ++i) { var threadInfo = new ThreadInfo(); threadInfo.ThreadId = i; threadInfo.MainThreadQueue = mainThreadQueue; threadInfo.CreateCount = Random.Range(5, 20); var thread = new Thread(threadStart); thread.Start(threadInfo); } } void Update() { // Execute commands for up to 5 milliseconds mainThreadQueue.Execute(5); } static void OtherThread(object startParam) { // Create game objects and set their transforms' positions ThreadInfo threadInfo = (ThreadInfo)startParam; var mainThreadQueue = threadInfo.MainThreadQueue; var newGameObjectResult = new MainThreadQueue.Result<GameObject>(); var getTransformResult = new MainThreadQueue.Result<Transform>(); var setPositionResult = new MainThreadQueue.Result(); for (var i = 0; i < threadInfo.CreateCount; ++i) { // New game object var name = "From Thread " + threadInfo.ThreadId; mainThreadQueue.NewGameObject(name, newGameObjectResult); var go = newGameObjectResult.Value; // Get game object's transform mainThreadQueue.GetTransform(go, getTransformResult); var transform = getTransformResult.Value; // Set transform's position var pos = new Vector3(i, i, i); mainThreadQueue.SetPosition(transform, pos, setPositionResult); setPositionResult.Wait(); } } }
There are a couple more aspects of this to notice. First, the Result
objects are reused by the threads. There’s no need to pool them in this case and the worker thread didn’t even need to call Reset
because MainThreadQueue
takes care of that when you queue a command.
Also, when the script’s Update
runs MainThreadQueue.Execute
it can pass in a time limit. This allows us to put a cap on how much time we want to spend executing commands per frame. We can use this to easily spread out work across multiple frames because the remaining work simply sits in the queue to be executed on the next Update
of the script.
Now let’s look at the full source code for this system. It’s heavily-commented, but still under 500 lines in just one file. Feel free to drop it in your projects as it’s MIT-licensed.
using System.Collections.Generic; using System.Diagnostics; using System.Threading; using UnityEngine; using UnityEngine.Assertions; /// <summary> /// A queue of commands to execute on the main thread. Each command function (e.g. NewGameObject) /// takes a Result parameter that initially has no value but gets a value after the command /// executes. /// </summary> /// <author>http://JacksonDunstan.com/articles/3930</author> /// <license>MIT</license> public class MainThreadQueue { /// <summary> /// Result of a queued command. Will have a Value when it IsReady. /// </summary> public class Result<T> { private T value; private bool hasValue; private AutoResetEvent readyEvent; public Result() { readyEvent = new AutoResetEvent(false); } /// <summary> /// Result value. Blocks until IsReady is true. /// </summary> public T Value { get { readyEvent.WaitOne(); return value; } } /// <summary> /// Check if the result value is ready. /// </summary> public bool IsReady { get { return hasValue; } } /// <summary> /// Set the result value and flag it as ready. /// This is meant to be called by MainThreadQueue only. /// </summary> /// <param name="value"> /// The result value /// </param> public void Ready(T value) { this.value = value; hasValue = true; readyEvent.Set(); } /// <summary> /// Reset the result so it can be used again. /// </summary> public void Reset() { value = default(T); hasValue = false; } } /// <summary> /// A result with no value (i.e. for a function returning "void") /// </summary> public class Result { private bool hasValue; private AutoResetEvent readyEvent; public Result() { readyEvent = new AutoResetEvent(false); } /// <summary> /// If the command has been executed /// </summary> public bool IsReady { get { return hasValue; } } /// <summary> /// Mark the result as ready to indicate that the command has been executed. /// </summary> public void Ready() { hasValue = true; readyEvent.Set(); } /// <summary> /// Blocks until IsReady is true /// </summary> public void Wait() { readyEvent.WaitOne(); } /// <summary> /// Reset the result so it can be used again. /// </summary> public void Reset() { hasValue = false; } } /// <summary> /// Types of commands /// </summary> private enum CommandType { /// <summary> /// Instantiate a new GameObject /// </summary> NewGameObject, /// <summary> /// Get a GameObject's transform /// </summary> GetTransform, /// <summary> /// Set a Transform's position /// </summary> SetPosition } /// <summary> /// Base class of all command types /// </summary> private abstract class BaseCommand { /// <summary> /// Type of the command /// </summary> public CommandType Type; } /// <summary> /// Command object for instantiating a GameObject /// </summary> private class NewGameObjectCommand : BaseCommand { /// <summary> /// Name of the GameObject /// </summary> public string Name; /// <summary> /// Result of the command: the newly-instantiated GameObject /// </summary> public Result<GameObject> Result; public NewGameObjectCommand() { Type = CommandType.NewGameObject; } } /// <summary> /// Command object for getting a GameObject's transform /// </summary> private class GetTransformCommand : BaseCommand { /// <summary> /// GameObject to get the Transform for /// </summary> public GameObject GameObject; /// <summary> /// Result of the command: the GameObject's transform. /// </summary> public Result<Transform> Result; public GetTransformCommand() { Type = CommandType.GetTransform; } } /// <summary> /// Set a Transform's position /// </summary> private class SetPositionCommand : BaseCommand { /// <summary> /// Transform to set the position of /// </summary> public Transform Transform; /// <summary> /// Position to set to the Transform /// </summary> public Vector3 Position; /// <summary> /// Result of the command: no value /// </summary> public Result Result; public SetPositionCommand() { Type = CommandType.SetPosition; } } // Pools of command objects used to avoid creating more than we need private Stack<NewGameObjectCommand> newGameObjectPool; private Stack<GetTransformCommand> getTransformPool; private Stack<SetPositionCommand> setPositionPool; // Queue of commands to execute private Queue<BaseCommand> commandQueue; // Stopwatch for limiting the time spent by Execute private Stopwatch executeLimitStopwatch; /// <summary> /// Create the queue. It initially has no commands. /// </summary> public MainThreadQueue() { newGameObjectPool = new Stack<NewGameObjectCommand>(); getTransformPool = new Stack<GetTransformCommand>(); setPositionPool = new Stack<SetPositionCommand>(); commandQueue = new Queue<BaseCommand>(); executeLimitStopwatch = new Stopwatch(); } /// <summary> /// Get an object from a pool or create a new one if none are available. /// This function is thread-safe. /// </summary> /// <returns> /// An object from the pool or a new instance /// </returns> /// <param name="pool"> /// Pool to get from /// </param> /// <typeparam name="T"> /// Type of pooled object /// </typeparam> private static T GetFromPool<T>(Stack<T> pool) where T : new() { lock (pool) { if (pool.Count > 0) { return pool.Pop(); } } return new T(); } /// <summary> /// Return an object to a pool. /// This function is thread-safe. /// </summary> /// <param name="pool"> /// Pool to return to /// </param> /// <param name="obj"> /// Object to return /// </param> /// <typeparam name="T"> /// Type of pooled object /// </typeparam> private static void ReturnToPool<T>(Stack<T> pool, T obj) { lock (pool) { pool.Push(obj); } } /// <summary> /// Queue a command. This function is thread-safe. /// </summary> /// <param name="cmd"> /// Command to queue /// </param> private void QueueCommand(BaseCommand cmd) { lock (commandQueue) { commandQueue.Enqueue(cmd); } } /// <summary> /// Queue a command to instantiate a GameObject /// </summary> /// <param name="name"> /// Name of the GameObject. Must not be null. /// </param> /// <param name="result"> /// Result to be filled in when the command executes. Must not be null. /// </param> public void NewGameObject( string name, Result<GameObject> result) { Assert.IsTrue(name != null); Assert.IsTrue(result != null); result.Reset(); NewGameObjectCommand cmd = GetFromPool(newGameObjectPool); cmd.Name = name; cmd.Result = result; QueueCommand(cmd); } /// <summary> /// Queue a command to get a GameObject's transform /// </summary> /// <param name="go"> /// GameObject to get the transform from. Must not be null. /// </param> /// <param name="result"> /// Result to be filled in when the command executes. Must not be null. /// </param> public void GetTransform( GameObject go, Result<Transform> result) { Assert.IsTrue(go != null); Assert.IsTrue(result != null); result.Reset(); GetTransformCommand cmd = GetFromPool(getTransformPool); cmd.GameObject = go; cmd.Result = result; QueueCommand(cmd); } /// <summary> /// Queue a command to set a Transform's position /// </summary> /// <param name="transform"> /// Transform to set the position of /// </param> /// <param name="position"> /// Position to set to the transform /// </param> /// <param name="result"> /// Result to be filled in when the command executes. Must not be null. /// </param> /// <param name="result"> /// Result to be filled in when the command executes. Must not be null. /// </param> public void SetPosition( Transform transform, Vector3 position, Result result) { Assert.IsTrue(transform != null); Assert.IsTrue(result != null); result.Reset(); SetPositionCommand cmd = GetFromPool(setPositionPool); cmd.Transform = transform; cmd.Position = position; cmd.Result = result; QueueCommand(cmd); } /// <summary> /// Execute commands until there are none left or a maximum time is used /// </summary> /// <param name="maxMilliseconds"> /// Maximum number of milliseconds to execute for. Must be positive. /// </param> public void Execute(int maxMilliseconds = int.MaxValue) { Assert.IsTrue(maxMilliseconds > 0); // Process commands until we run out of time executeLimitStopwatch.Reset(); executeLimitStopwatch.Start(); while (executeLimitStopwatch.ElapsedMilliseconds < maxMilliseconds) { // Get the next queued command, but stop if the queue is empty BaseCommand baseCmd; lock (commandQueue) { if (commandQueue.Count == 0) { break; } baseCmd = commandQueue.Dequeue(); } // Process the command. These steps are followed for each command: // 1. Extract the command's fields // 2. Reset the command's fields // 3. Do the work // 4. Return the command to its pool // 5. Make the result ready switch (baseCmd.Type) { case CommandType.NewGameObject: { // Extract the command's fields NewGameObjectCommand cmd = (NewGameObjectCommand)baseCmd; string name = cmd.Name; Result<GameObject> result = cmd.Result; // Reset the command's fields cmd.Name = null; cmd.Result = null; // Return the command to its pool ReturnToPool(newGameObjectPool, cmd); // Do the work GameObject go = new GameObject(name); // Make the result ready result.Ready(go); break; } case CommandType.GetTransform: { // Extract the command's fields GetTransformCommand cmd = (GetTransformCommand)baseCmd; GameObject go = cmd.GameObject; Result<Transform> result = cmd.Result; // Reset the command's fields cmd.GameObject = null; cmd.Result = null; // Return the command to its pool ReturnToPool(getTransformPool, cmd); // Do the work Transform transform = go.transform; // Make the result ready result.Ready(transform); break; } case CommandType.SetPosition: { // Extract the command's fields SetPositionCommand cmd = (SetPositionCommand)baseCmd; Transform transform = cmd.Transform; Vector3 position = cmd.Position; Result result = cmd.Result; // Reset the command's fields cmd.Transform = null; cmd.Position = Vector3.zero; cmd.Result = null; // Return the command to its pool ReturnToPool(setPositionPool, cmd); // Do the work transform.position = position; // Make the result ready result.Ready(); break; } } } } }
The above code is just a starting place for exposing the Unity API to worker threads. It only supports the three commands used in the test script: “new game object”, “get game object’s transform”, “set transform’s position”. To be useful in a real game, you’ll need to add many more commands. Thankfully, that’s easy!
First, add a new entry to the CommandType
enum for your new command. Then add a new class extending BaseCommand
. Use the other command classes as a pattern. Remember to make it private
and set Type = CommandType.MyNewCommand
in the constructor.
Next, add a new pool field and initialize it in the constructor. Just copy/paste from the others and change the command type.
Now add a new MyCommand
function to queue the command. You can copy/paste one of the others and change the specifics. It’s mostly just boilerplate code copying parameters into the fields of the command.
Finally, add a new case CommandType.MyNewCommand
to Execute
. Follow the five steps listed above the switch
or use the other cases as a pattern.
This is really just wrapper code, so it’s pretty mindless work to expose more Unity API functionality as commands and results. There’s no need to expose the whole Unity API, just the parts you need to use from other threads.
And that’s about all there is to the system. It should be easy to drop into projects and eliminate what seems to be the main reason that Unity programmers don’t use threads. So let’s go put those CPU cores to work!
#1 by pretender on July 3rd, 2017 ·
Great article! Is this safe to be used on mobile devices?
#2 by jackson on July 3rd, 2017 ·
I just put the sample from the article on an Android device to test. It seems to work just fine on mobile devices.
#3 by Theodoros Doukoulos on July 3rd, 2017 ·
Hello there,
I am not an expert in threads but i am following you your webposts quite closely because i like the content.
So i shared your post on my facebook and i’ve got a comment from one of my friends as an enquiry of performance tests with this pattern.
Well the main context of his concern was the and if that is the case then the code would be slower if we would use them for each call on the main thread then the code would be slower than wiring it all up in the main thread instead.
Can you please provide an example with some performance tests on that matter? Thanks in advance.
#4 by jackson on July 3rd, 2017 ·
Performance will depend a lot on your Unity API usage. If your code is 100% calls to the Unity API, then there’s no real point in using threads since you’re just adding overhead for the queue. But if your code is 1% calls to the Unity API then the other 99% will benefit from using more CPU cores. In that case the overhead of the queue will be dwarfed by the speed boost you get from the threads. Somewhere in between there’s an equilibrium point where it doesn’t matter.
Exactly how much of your code needs to not be using the Unity API in order for this to be an overall performance win will really depend on the specifics of your code. I can’t think of a good performance test that wouldn’t be terribly contrived, but let me know if you have any ideas for one. I’d venture to say that many games will benefit tremendously from multi-threading as usually there’s a lot of code to do game logic and then a little code to output the result via the Unity API.
#5 by Lauraaa on February 25th, 2018 ·
Thank you for the detailed tutorial!
I used it to create new Texture2D in thread, works great!