Coroutines are great for tasks that are easy to break up into little chunks, but we still need threads for long-running blocking calls. Today’s article shows how you can mix some threads into your coroutines to easily combine these two kinds of asynchronous processes.

To review, a coroutine suspends every time you use yield return and is resumed by Unity once per frame:

using System.Collectons;
using UnityEngine;
 
class TestScript : MonoBehaviour
{
	class Config
	{
		public string Version;
		public string AssetsUrl;
	}
 
	void Start()
	{
		StartCoroutine(LoadConfig());
	}
 
	IEnumerator LoadConfig()
	{
		// Load the config on the first frame
		string json = File.ReadAllText("/path/to/config.json");
 
		// Wait until the second frame
		yield return null;
 
		// Parse the config on the second frame
		Config config = JsonUtility.FromJson<Config>(json);
 
		// Wait until the third frame
		yield return null;
 
		// Use the config on the third frame
		Debug.Log("Version: " + config.Version + "\nAssets URL: " + config.AssetsUrl);
	}
}

The LoadConfig coroutine breaks up the task into three steps: load the config JSON file, parse the config JSON file into a Config object, and use the config. It uses yield return to make sure these three tasks happen over three frames so no one frame is saddled with the burden of all the work.

Splitting up the task this way is good practice, but it won’t scale very well. Each of the three steps could individually take longer than we want to spend on a single frame. To work around it we’ll need to further break down the task. For example, instead of using File.ReadAllText in the “load” step we might open a FileStream and load just part of the file each frame. It’ll quickly become quite complex, but we’ll be able to load much larger JSON files while spreading the work over several frames.

using System.Collections;
using System.IO;
using System.Text;
using UnityEngine;
 
public class TestScript : MonoBehaviour
{
	class Config
	{
		public string Version;
		public string AssetsUrl;
	}
 
	void Start()
	{
		StartCoroutine(LoadConfig());
	}
 
	IEnumerator LoadConfig()
	{
		// Load 1 KB of the config on each frame until it's loaded
		MemoryStream jsonStream = new MemoryStream();
		byte[] buffer = new byte[1024];
		using (FileStream fileStream = File.OpenRead("/path/to/config.json"))
		{
			while (true)
			{
				int numBytesRead = fileStream.Read(buffer, 0, buffer.Length);
				if (numBytesRead == 0)
				{
					break;
				}
				jsonStream.Write(buffer, 0, numBytesRead);
				yield return null;
			}
		}
 
		// Wait until the next frame and parse the string
		yield return null;
		string json = Encoding.UTF8.GetString(jsonStream.ToArray());
 
		// Wait until the next frame and parse the config
		yield return null;
		Config config = JsonUtility.FromJson<Config>(json);
 
		// Wait until the next frame and use the config
		yield return null;
		Debug.Log("Version: " + config.Version + "\nAssets URL: " + config.AssetsUrl);
	}
}

This version loads 1 KB of the config JSON per frame to make sure that not too much time is spent loading a huge JSON document. We might be able to find a way to split up the JSON parsing, but it would probably be exceedingly ugly. The “use” step is just a Debug.Log in this example, so we’ll skip analyzing that one.

The point is that splitting up these processes so that just one chunk is executed per frame adds a lot of complexity and a lot of work for the programmer. An alternative is to not split up the work and instead run it on another thread. This works well if there is an idle core that could be performing this task. Even if there isn’t, the OS will split the CPU time between the thread and whatever else was running. This can even be controlled by the System.Threading.ThreadPriority enum to create low- and high-priority threads.

Creating threads in C# is simple. Just create a System.Threading.Thread and call its Start.

using System.Collections;
using System.IO;
using System.Threading;
using UnityEngine;
 
public class TestScript : MonoBehaviour
{
	class Config
	{
		public string Version;
		public string AssetsUrl;
	}
 
	void Start()
	{
		StartCoroutine(LoadConfig());
	}
 
	IEnumerator LoadConfig()
	{
		// Start a thread on the first frame
		Config config = null;
		bool done = false;
		new Thread(() => {
			// Load and parse the JSON without worrying about frames
			string json = File.ReadAllText("/path/to/config.json");
			config = JsonUtility.FromJson<Config>(json);
			done = true;
		}).Start();
 
		// Do nothing on each frame until the thread is done
		while (!done)
		{
			yield return null;
		}
 
		// Use the config on the first frame after the thread is done
		Debug.Log("Version: " + config.Version + "\nAssets URL: " + config.AssetsUrl);
	}
}

Notice how there’s no longer a need to break down each step into tiny pieces. We can use the convenient File.ReadAllText and not worry about splitting up JsonUtility.FromJson. We don’t even insert a pause between these two steps. Likewise, we could put the “use” step in the thread too, provided we didn’t need to access the Unity API of course. The Unity API is mostly only accessible from the main thread.

With this technique set, let’s formalize it for easy reuse. A CustomYieldInstruction is the perfect tool for such a job:

using System;
using System.Threading;
 
/// <summary>
/// A CustomYieldInstruction that executes a task on a new thread and keeps waiting until it's done.
/// http://JacksonDunstan.com/articles/3746
/// </summary>
class WaitForThreadedTask : UnityEngine.CustomYieldInstruction
{
	/// <summary>
	/// If the thread is still running
	/// </summary>
	private bool isRunning;
 
	/// <summary>
	/// Start the task by starting a thread with the given priority. It immediately executes the
	/// given task. When the given task finishes, <see cref="keepWaiting"/> returns true.
	/// </summary>
	/// <param name="task">Task to execute in the thread</param>
	/// <param name="priority">Priority of the thread to execute the task in</param>
	public WaitForThreadedTask(
		Action task,
		ThreadPriority priority = ThreadPriority.Normal
	)
	{
		isRunning = true;
		new Thread(() => { task(); isRunning = false; }).Start(priority);
	}
 
	/// <summary>
	/// If the coroutine should keep waiting
	/// </summary>
	/// <value>If the thread is still running</value>
	public override bool keepWaiting { get { return isRunning; } }
}

Now we can use WaitForThreadedTask like this:

using System.Collections;
using System.IO;
using UnityEngine;
 
public class TestScript : MonoBehaviour
{
	class Config
	{
		public string Version;
		public string AssetsUrl;
	}
 
	void Start()
	{
		StartCoroutine(LoadConfig());
	}
 
	IEnumerator LoadConfig()
	{
		Config config = null;
		yield return new WaitForThreadedTask(() => {
			string json = File.ReadAllText("/path/to/config.json");
			config = JsonUtility.FromJson<Config>(json);
		});
		Debug.Log("Version: " + config.Version + "\nAssets URL: " + config.AssetsUrl);
	}
}

WaitForThreadedTask helps clean up the code a little bit since we no longer have a while loop or a done variable. We just yield return it with our task we want to run in the thread and we’ll be resumed when the task is complete. If we want, we can even set the thread’s priority (e.g. to Highest or Lowest) by passing in a ThreadPriority enumeration value to the WaitForThreadedTask constructor.

Hopefully you’ll find WaitForThreadedTask useful. Let me know in the comments section if you’ll try it out or how it worked if you’ve done anything similar in your own projects.