Too Many Coroutines: A Queue Solution
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!
#1 by Yuri Tikhomirov on November 18th, 2015 ·
Hey, nice job!
Shouldn’t we call Dispose() on the ‘coroutine enumerator’ after all?..
#2 by jackson on November 18th, 2015 ·
Thanks! :)
There’s an interesting subtlety to
IEnumerator
: it doesn’t provide aDispose
method as you can see from the documentation. However, its sibling interfaceIEnumerator<T>
does have aDispose
method, as mentioned in the documentation.This means that it’s not possible for
CoroutineQueue
to callDispose
without casting or reflection, so it probably shouldn’t. An alternative would be forCoroutineQueue
to also takeIEnumerator<T>
instances. I think that’d be a bit strange though since Unity’s coroutines are all built around the non-genericIEnumerator
. If it were anIteratorQueue
then that might make more sense.For more about
IEnumerator
andIEnumerator<T>
, including some discussion of theirDispose
, check out my article Do Foreach Loops Create Garbage?#3 by Cesar on July 17th, 2016 ·
You saved me, really great work!
#4 by onesoftgames on December 14th, 2016 ·
Hi @jackson,
Your articles are all awesome! This CoroutineQueue class is exactly what i need.
I found a small bug if we try to call Coroutine without parameter, like this:
In log console, we can see “end TestCoroutine” only print one. Maybe without params, unity treat 2 instance of our coroutine as one? In this case, is there any solution?
(If we call TestCoroutine with params e.x id , it is print two times as we expected.)
#5 by jackson on December 15th, 2016 ·
Glad you’re enjoying the articles! Let me know if you have any ideas for articles you’d like to see. I’m always looking for topics that readers will appreciate. :)
I copied your code into Unity 5.5.0f3 and ran it with the same
CoroutineQueue
from the article and it printed all the logs for both coroutines, including the “end TestCoroutine” ones. Any tips on how to reproduce your issue?#6 by onesoftgames on December 18th, 2016 ·
Opp! I am sorry, it is my mistake.
In Console View, the Collapse feature is turn on. That why they group two message into one and show small “2” number on the right.
I just start working with unity so I can not see it from start :p
Thank so much!
#7 by onesoftgames on December 19th, 2016 ·
And about ideas for articles, It will be great if you can compare and bring the best solution for
“Event System – Dispatcher” System. Event and message dispatching is a critical part of any game!
Thank so much!
#8 by jackson on December 19th, 2016 ·
Thanks for the idea! I’ve done a few articles about event and callback performance:
Event Performance: C# vs. UnityEvent
Fastest Callbacks: Delegate, Event, or Interface?
Unity Function Performance Followup
Was there something specific you’d like me to cover that I haven’t so far?
#9 by onesoftgames on December 20th, 2016 ·
Thanks! I learn a lot from these articles.
For the ideas, I mean something like this:
https://www.assetstore.unity3d.com/en/#!/content/12715
Can you create a simple but very fast event system?
#10 by jackson on December 20th, 2016 ·
Thanks for the link. I haven’t used that library before, but I can guess how it works based on the documentation. The
MessageDispatcher.AddListener
function takes a delegate that they say looks like this:Since delegates and C# events are very similar, you can expect the performance to be no better than delegates or events in any of those articles I linked. It should be a ton faster than
GameObject.SendMessage
unless the string filtering or time delay features impose some huge overhead, but probably not.As an aside, I’d personally prefer to not use strings for the event name and to strongly type my event parameters. If you prefer that too, you can get most of the benefits of that library by simply declaring static events:
Then use it similarly to their example:
Thanks for the article idea. I may very well write an article about higher-level event systems than just events and delegates.
#11 by onesoftgames on December 20th, 2016 ·
Thank you so much! I can’t wait to see your next articles!
#12 by Tristan on November 8th, 2018 ·
This is very useful. Thank you for sharing.
#13 by MM on April 17th, 2019 ·
I needed to implement a generic coroutine queue and this fits the bill. Thank you so much
#14 by Arch on May 15th, 2019 ·
Hi, I used your solution for a long time now, I just noticed that the recursive calling can bring to very large overhead when queuing hundreds of coroutines and loging in them.
Would you have an elegant non recursive solution ?
I added a while(true) corroutine in my solution but I find it quite clumsy.
Thanks
#15 by jackson on May 16th, 2019 ·
Which “recursive calling” are you referring to?
#16 by christian on October 29th, 2019 ·
This looks very interesting.
Is there any way to pause/stop the queue?
Is there any way to insert a job at the top of the queue?
#17 by jackson on October 31st, 2019 ·
These features aren’t supported by the code in the article, but you could certainly modify the code to implement these pretty easily.
#18 by Jay on June 7th, 2023 ·
Hi there! I really love this system! Thank you so much for sharing this and it’s working great so far. I have a question, I’m using this for cloud save and changes made locally will be pushed to the queue to be saved in the cloud with max being 1 coroutine at a time. I’m not so good with coroutines, how can I have it like when a coroutine is running, if another one is added to the queue we check if yet another one was added then remove the recent added one from queue?
So in other words,
> [1] changes made to wallet is added being pushed to cloud (coroutine running)
> [2] added other coroutine that pushes the item bought from the money from the wallet (in queue)
> [3] yet another coroutine added that pushes which inventory the item bought is being added to (in queue)
How can I remove [2] from queue and only run the latest one? So in general if many are added in a short period of time, how can I run only the latest one?
What I’m doing is not a generic one but building off your system but don’t really know how.
#19 by jackson on June 7th, 2023 ·
I’m glad you’re liking it! One way to add the feature you’re looking for would be to create a struct containing the
IEnumerator
and either an ID as essentially metadata for the coroutine so you can compare it against others for the purposes of de-duplication. For example, here’s an untested modified version of the code from the article:Alternatively, you could omit the struct and compare
IEnumerator
references if you’re really sure that they’ll always be the same for every coroutine of the same “type”/”id”.