How do Unity’s coroutines actually work?

Oliver Oliver • Published 3 years ago Updated 2 years ago


Coroutines in Unity are a way to run expensive loops, or delays in execution, without having to involve multithreading. That's right – although it's commonly believed that coroutines are multithreaded operations, they in fact run on the main thread. But have you ever asked yourself why coroutines return IEnumerator? What does that even mean? We'll take a look at how they work, and I hope to explain just how genius they are.

Coroutines are not multithreading

You can prove this to yourself by calling Thread.Sleep from within a coroutine.

using System.Collections;
using System.Threading;
using UnityEngine;
 
public class Coroutines : MonoBehaviour
{
    private void Start()
    {
        StartCoroutine(MySimpleCoroutine());
    }
 
    private IEnumerator MySimpleCoroutine()
    {
        Debug.Log("Hello from the coroutine!");
        Thread.Sleep(5000);
        
        yield break; // necessary for IEnumerator
    }
}

Attach this behaviour to a game object, and you will see that execution sleeps for 5 seconds on the current thread – which is the main thread. The game freezes entirely. If coroutines ran on a separate threads, this Sleep call would not interfere with rendering.

What does it mean to yield?

Let's step away from Unity and start a blank Console Application, and explain what makes the yield keyword so special.

Let's define a method which returns IEnumerable<int> called GetNumbers. This method will allocate a list and then loop 10 times. In the loop we will add the current loop iteration to the list, and then Thread.Sleep for 1 second. Afterwards, we will return the list.

using System.Collections.Generic;
using System.Threading;
 
internal static class Program
{
    private static IEnumerable<int> GetNumbers()
    {
        var list = new List<int>();
 
        for (int iterator = 1; iterator <= 10; iterator++)
        {
            list.Add(iterator);
            Thread.Sleep(1000); // 1 second
        }
 
        return list.ToArray();
    }
}

Now, let's call this method from Main and print the results to the console.

using System.Collections.Generic;
using System.Threading;
 
internal static class Program
{
    private static void Main()
    {
        <mark>IEnumerable<int> numbers = GetNumbers();</mark>
        <mark>foreach (var number in GetNumbers)</mark>
        <mark>{</mark>
        <mark>    Console.WriteLine(number);</mark>
        <mark>}</mark>
    }

    private static IEnumerable<int> GetNumbers()
    {
        var list = new List<int>();
 
        for (int iterator = 1; iterator <= 10; iterator++)
        {
            list.Add(iterator);
            Thread.Sleep(1000); // 1 second
        }
 
        return list.ToArray();
    }
}

Running this code, you will notice something. It waits 10 seconds before finally outputting the values 1-10 immediately, all at once. This is because in order to foreach over a collection, it has to know what that collection is. But it can only know what the collection is once the method returns. But the method only returns once its for loop is complete, which means it has to Thread.Sleep 10 times, 1 second each, adding each element to the list before the method finally gives the result back to the caller: our foreach.

Let's adjust the code to remove the creation of the list, and the return statement at the end. Instead, place a yield return statement inside the loop. We will return the value of iterator. The GetNumbers method now looks like this:

private static IEnumerable<int> GetNumbers()
{
    for (int iterator = 1; iterator <= 10; iterator++)
    {
        <mark>yield return iterator;</mark>
        Thread.Sleep(1000); // 1 second
    }
}

The foreach loop in Main does not need to be touched – we still want to iterate over the result of this method. Except, running this code, you will immediately see the difference this made.

Instead of sleeping 1 second – 10 times – before finally returning control back to the caller (leading to a 10 second sleep in total), what we are doing is quite literally “yielding” control back to the caller with the next value of iterator in every iteration of the for loop. This gives a chance for the foreach body to execute, print the value, and then return back to GetNumbers to essentially ask “what next?” – at which point the current thread sleeps for 1 second, and the for loop continues to the next value.

Wait. That's an IEnumerable. Unity uses IEnumerator!

Correct it does! IEnumerable is the base interface for all things that are – well – “enumerable”. Lists, arrays, queues, stacks, dictionaries, anything which can be “enumerated” implements IEnumerable. IEnumerator, on the other hand, is a type which is responsible for defining how such enumerables should be enumerated.

In short, IEnumerable is a collection of values. IEnumerator is responsible for iterating over such collections.

So how does this relate to coroutines?

Diving deep into how foreach works

You may have noticed that types such as arrays and lists all define a GetEnumerator method. This method returns a type which implements IEnumerator, and it's responsible for implementing the way that the current collection must be enumerated.

Save for arrays, because those compile slightly differently, a foreach actually compiles to a while loop which runs for as long as IEnumerator.MoveNext returns true. You can see this in action here!

Ignoring the initialisation and try-catch, our source code:

foreach (int number in list)
{
    Console.WriteLine(number);
}

...compiles to:

List<int>.Enumerator enumerator = list.GetEnumerator();
while (enumerator.MoveNext())
{
    Console.WriteLine(enumerator.Current);
}
enumerator.Dispose();

What is actually happening here, and why does it work?

We can browse the .NET source code for the List<int>.Enumerator struct to see what's going on. Let's remove all the noise, clean it up so it's more readable, and just focus on the important parts.

public struct Enumerator : IEnumerator<T>
{
    private readonly List<T> _list;
    private int _index;
    private T _current;
 
    public T Current => _current;
 
    public bool MoveNext()
    {
        if (_index < _list.Count)
        {
            _current = _list[_index];
            _index++;
            return true;
        }
 
        _index = _list.Count + 1;
        _current = default;
        return false;
    }
 
    // ... more code here. not important to focus on
}

We see that the enumerator tracks the current index, as well as the value at that index. The Current property just returns the value of the _current field. But every call to MoveNext checks if the index is within the bounds of the list. If it is, it assigns _current to the value at _index, increments _index, and returns true.

So the while loop we saw above is simply asking the enumerator to move to the next index, and then accesses Current. Until, of course, _index reaches the end of the list, at which point it returns false and the loop is complete.

A Stack<T> enumerator behaves a similar way, except it iterates backwards. Looking at the source code for StackEnumerator, line 361 shows a decrement: --_index. This is the reason a foreach calls GetEnumerator – because different collection types need to be enumerated differently.

What does this have to do with coroutines?

Whenever you call StartCoroutine, Unity adds the IEnumerator to a collection of coroutines on which it needs to call MoveNext in every iteration of its own game loop. We can see this in action if we were to create our own yield instruction in Unity. Below is an example of a custom WaitForTime – which accepts a TimeSpan to indicate how long execution should be paused for.

Essentially, it is a custom version of WaitForSecondsRealtime, but instead accepting a TimeSpan instead of a float for the duration.

The constructor calculates the end time by adding the TimeSpan parameter to the current time. Then in MoveNext, we simply check whether or not the current time has elapsed the end time. It returns true for as long as _endTime is in the future.

using System;
using System.Collections;
 
public class WaitForTime : IEnumerator
{
    private readonly DateTime _endTime;
 
    public WaitForTime(TimeSpan time)
    {
        _endTime = DateTime.Now + time;
    }
 
    /// <inheritdoc />
    public bool MoveNext() => DateTime.Now < _endTime;
 
    /// <inheritdoc />
    public void Reset() { } // not important
 
    /// <inheritdoc />
    public object Current => null;
}

If we yield return a new instance of this in a coroutine, it works as expected:

private IEnumerator MySimpleCoroutine()
{
    Debug.Log("Hello from the coroutine!");
    yield return new WaitForTime(TimeSpan.FromSeconds(5));
    Debug.Log("Slept for 5 seconds!");
}

It's nice when things work the first time.

We can shove in a few Debug.Log calls to see when Unity is calling things.

Current is being accessed as a way to support nested coroutines.

You can see the message count for the MoveNext call shoots up to the thousands extremely quickly. This is because Unity is calling MoveNext on every coroutine in the collection every frame.

No, seriously, coroutines are not multithreading

Despite the names of the types – WaitForSeconds, WaitForSecondsRealtime, WaitForEndOfFrame, and our very own WaitForTime – containing the word “wait”, there's no actual waiting going on here; MoveNext is called every single frame and there is zero pause between each call (we can see evidence of that in our Console). The only reason there is an apparent pause before “Slept for 5 seconds!” is logged is because of the yield. We constantly yield control back to Unity's game loop, and so long as our instruction's MoveNext returns true, control never continues beyond that point. At no point do Wait instructions hang the thread. There is no Thread.Sleep call.

As an aside, that means – if you really wanted to, you could create an endless instruction which “waits” forever, just by defining MoveNext to unconditionally return true.

public class WaitForever : IEnumerator
{
    public bool MoveNext() => true;
 
    public void Reset() { }
 
    public object Current => null;
}

Conclusion

I was planning to write up a small example project which implemented a system very similar to Unity's coroutines to demonstrate how Unity's game loop actually handles them. While I did get it working, the code ended up being far more long-winded than I originally anticipated. Frankly, this makes me respect coroutines even more; I underestimated the level of wizardry that they involved. So much so that I felt it went far beyond the scope of this article.

However! If enough of you are interested in another post in which I elaborate on that code, perhaps I'll write about it in future. Let me know in the comments.

While coroutines have their pitfalls – and trust me there are many – they certainly are a very clever feature. One which I feel is often underappreciated.

Update 21 January 2022

I wrote a follow-up post! You can read it here.