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.

Thread Communication Diagram

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!