Unity’s coroutine support is great. So great that it’s easy to go overboard and end up with too many of them. That could be for any number of reasons. Perhaps the coroutines are using too much memory or have too many files open at once. In any case, you’ll need to find a way to limit how many are running at a single time. Today’s article introduces a solution to the problem that queues coroutines so you never have too many running. Read on to learn about the solution and for the class that implements it.

The following queue solution is simple in concept but has some tricky elements that are vital to make it work. All we’re going to do is run coroutines until there are too many running, queue any excess coroutines, and run them when the first coroutines finish. Sounds easy!

The biggest issue with this approach is this bit: “when the first coroutines finish”. How do you know when a coroutine has finished? Do you get a callback? Is an event dispatched? No, you don’t get notified by Unity in any of these ways. The normal approach is to put your “coroutine finished” code in the coroutine itself like this:

IEnumerator DownloadText(string url, Action<string> callback)
{
	// Do the coroutine's work
	var www = new WWW(url);
	yield return www;
 
	// Notify that the coroutine is finished
	callback(www.text);
}

This works great for specific coroutines, but the coroutine queueing system should work for any coroutine. It can’t know about the specific function signatures of coroutines like DownloadText, so we need another way to find out if the coroutine is done.

A better way is for the queueing system to run its own coroutine instead of the one the user wants it to run. This way it can be notified when the coroutine is finished. Something like this:

IEnumerator QueueSystemCoroutine(IEnumerator userCoroutine, Action callback)
{
	// ... Run the user's coroutine (e.g. DownloadText)
 
	// Notify that the coroutine is finished
	callback();
}

So how do we run the user’s coroutine? Luckily, coroutines are just an IEnumerator so it’s easy to loop over them and yield return their values:

IEnumerator QueueSystemCoroutine(IEnumerator userCoroutine, Action callback)
{
	// Run the user's coroutine (e.g. DownloadText)
	while (userCoroutine.MoveNext())
	{
		yield return userCoroutine.Current;
	}
 
	// Notify that the coroutine is finished
	callback();
}

Each time we call MoveNext the user’s coroutine is resumed and a bool is returned to indicate if it yielded anything. If it did, we can get it from the Current property. This means that the while loop basically just runs the user’s coroutine and yields all its values.

Now that we’ve solved the tricky part, let’s take a look at the class that implements the coroutine queue system:

using System;
using System.Collections;
using System.Collections.Generic;
 
using UnityEngine;
 
/// <summary>
/// Imposes a limit on the maximum number of coroutines that can be running at any given time. Runs
/// coroutines until the limit is reached and then begins queueing coroutines instead. When
/// coroutines finish, queued coroutines are run.
/// </summary>
/// <author>Jackson Dunstan, http://JacksonDunstan.com/articles/3241</author>
public class CoroutineQueue
{
	/// <summary>
	/// Maximum number of coroutines to run at once
	/// </summary>
	private readonly uint maxActive;
 
	/// <summary>
	/// Delegate to start coroutines with
	/// </summary>
	private readonly Func<IEnumerator,Coroutine> coroutineStarter;
 
	/// <summary>
	/// Queue of coroutines waiting to start
	/// </summary>
	private readonly Queue<IEnumerator> queue;
 
	/// <summary>
	/// Number of currently active coroutines
	/// </summary>
	private uint numActive;
 
	/// <summary>
	/// Create the queue, initially with no coroutines
	/// </summary>
	/// <param name="maxActive">
	/// Maximum number of coroutines to run at once. This must be at least one.
	/// </param>
	/// <param name="coroutineStarter">
	/// Delegate to start coroutines with. Normally you'd pass
	/// <see cref="MonoBehaviour.StartCoroutine"/> for this.
	/// </param>
	/// <exception cref="ArgumentException">
	/// If maxActive is zero.
	/// </exception>
	public CoroutineQueue(uint maxActive, Func<IEnumerator,Coroutine> coroutineStarter)
	{
		if (maxActive == 0)
		{
			throw new ArgumentException("Must be at least one", "maxActive");
		}
		this.maxActive = maxActive;
		this.coroutineStarter = coroutineStarter;
		queue = new Queue<IEnumerator>();
	}
 
	/// <summary>
	/// If the number of active coroutines is under the limit specified in the constructor, run the
	/// given coroutine. Otherwise, queue it to be run when other coroutines finish.
	/// </summary>
	/// <param name="coroutine">Coroutine to run or queue</param>
	public void Run(IEnumerator coroutine)
	{
		if (numActive < maxActive)
		{
			var runner = CoroutineRunner(coroutine);
			coroutineStarter(runner);
		}
		else
		{
			queue.Enqueue(coroutine);
		}
	}
 
	/// <summary>
	/// Runs a coroutine then runs the next queued coroutine (via <see cref="Run"/>) if available.
	/// Increments <see cref="numActive"/> before running the coroutine and decrements it after.
	/// </summary>
	/// <returns>Values yielded by the given coroutine</returns>
	/// <param name="coroutine">Coroutine to run</param>
	private IEnumerator CoroutineRunner(IEnumerator coroutine)
	{
		numActive++;
		while (coroutine.MoveNext())
		{
			yield return coroutine.Current;
		}
		numActive--;
		if (queue.Count > 0)
		{
			var next = queue.Dequeue();
			Run(next);
		}
	}
}

You’ll recognize QueueSystemCoroutine as CoroutineRunner, albeit with some of the queue system’s work built in. Now let’s see a little MonoBehaviour that uses CoroutineQueue:

public class TestScript : MonoBehaviour
{
	void Start()
	{
		// Create a coroutine queue that can run up to two coroutines at once
		var queue = new CoroutineQueue(2, StartCoroutine);
 
		// Try to run five coroutines
		for (var i = 0; i < 5; ++i)
		{
			queue.Run(TestCoroutine(i, 3));
		}
	}
 
	// A coroutine that logs its lifecycle and yields a given number of times
	IEnumerator TestCoroutine(int id, uint numYields)
	{
		Debug.Log("frame " + Time.frameCount + ": start " + id);
		for (var i = 0u; i < numYields; ++i)
		{
			Debug.Log("frame " + Time.frameCount + ": yield " + id);
			yield return null;
		}
		Debug.Log("frame " + Time.frameCount + ": end " + id);
	}
}

The test prints out this log (with annotations by me):

// starts the first coroutine (0)
frame 1: start 0
frame 1: yield 0
 
// starts the second coroutine (1)
frame 1: start 1
frame 1: yield 1
 
// next frame, the two running coroutines (0, 1) yield
frame 2: yield 0
frame 2: yield 1
 
// next frame, the two running coroutines (0, 1) yield again
frame 3: yield 0
frame 3: yield 1
 
// next frame, the first coroutine (0) ends
frame 4: end 0
 
// the first queued coroutine (2) starts
frame 4: start 2
frame 4: yield 2
 
// the second coroutine (1) ends
frame 4: end 1
 
// the second queued coroutine (3) starts
frame 4: start 3
frame 4: yield 3
 
// next frame, the two running coroutines (2, 3) yield
frame 5: yield 2
frame 5: yield 3
 
// next frame, the two running coroutines (2, 3) yield again
frame 6: yield 2
frame 6: yield 3
 
// next frame, the third coroutine (3) ends
frame 7: end 2
 
// the last queued coroutine (4) starts
frame 7: start 4
frame 7: yield 4
 
// the fourth coroutine (3) ends
frame 7: end 3
 
// next frame, the only remaining coroutine (4) yields
frame 8: yield 4
 
// next frame, the only remaining coroutine (4) yields
frame 9: yield 4
 
// next frame, the only remaining coroutine (4) ends
frame 10: end 4

You can see that the queue is accepting arbitrary coroutines, capping the number of them that run at once, detecting when they finish, and running queued coroutines until there are no more to run. So we have a working solution. Lots of embellishments could be made to it, such as allowing for the cap to change, dispatching events when coroutines finish or the queue is empty, making it implement an interface, and much more. Feel free to take the code and add the features you need. If you do, please post about it in the comments.

Do you use coroutines or C# iterators in your Unity projects? Did you ever need to limit how many were running? Post about your experiences in the comments!