Easy Threading With Coroutines
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.
#1 by Ed Earl on February 13th, 2017 ·
Neat! Makes threading look easy. Although, given the amount of time I seem to spend debugging beginners’ multithreaded code which naively mutates data from multiple threads without locks, I would always advocate for adding clear warnings about not doing that ;)
#2 by jackson on February 13th, 2017 ·
Threading can definitely be very challenging, especially if your threads are working on the same data and even more so if you’re not using locks. Nothing about
WaitForThreadedTask
will help you with that. It just makes setting up the thread and waiting for it to finish easier.#3 by pretender on February 17th, 2017 ·
Is it safe to use this on all platforms?
#4 by jackson on February 17th, 2017 ·
It should be fine as long as your platform supports threads. I’m not sure if that includes all platforms.
#5 by ms on February 22nd, 2017 ·
Crazy!
I love coroutines and use them probably too much — more than Update() — but never considered blending them with threads.
Simply awesome.
Thanks,
m
#6 by john on October 14th, 2020 ·
i am sorry but most of your posts are incorrect. in this one, coroutines are not threads. they run in mainthread. between Update and LateUpdate
#7 by jackson on October 14th, 2020 ·
This article never claims that coroutines are threads. It’s about how to use the two together, as stated in the opening paragraph:
Even if I disagree in this particular case, I strive for correctness in all my articles. Please post comments on any article if you ever think something is incorrect.
#8 by Hazy on February 28th, 2024 ·
Omg You legend. After Many research, just found your post. Succint, good, perfect for my problem. I had lag spikes loading my saved chunks in my game, this resolve all my problems !
BIG THANK YOU