Managing Coroutines in Unity Using a Job System

Coroutines are a powerful feature available to Unity Developers. Combined with the Unity API, coroutines can used to create animations, load assets, perform web requests and much more. Yet, as your project progresses, it becomes harder to manage all the different types of coroutines. What about managing coroutines in Unity using a specialized job system?

Managing coroutines in Unity using a Job System

What are Coroutines in Unity?

Coroutines are available to Unity Developers. As opposed to regular methods in C#, coroutines allow you to create methods that execute over time. This means the method is executed while the game is running, not blocking the next frame from being rendered. In Unity, they are used to perform asynchronous work such as web requests, loading levels and even managing complex animations.

How to Manage Coroutines in Unity?

One way of easily managing coroutines in Unity is by controlling them through a Job System or Job Manager. If you are familiar with the default Job System included in Unity then you might already be fully aware of its features and benefits. If not, then all you need to know is that a Job System is a system that processes little tasks. These tasks are usually referred to as Jobs and can be executed in a predetermined order. Similar to how a Job System manages a set of multithreaded jobs, we can create our own system to manage coroutines.

How to manage coroutines in Unity

Using Coroutines With The Unity Job System

Of course, you might ask why not just utilize the already existing Job System for this purpose? Well, that’s because the standard Job System is very specialized. Its main purpose is that of processing a lot of data in a multithreaded way. Unfortunately, at this point in time, coroutines are definitely not supported and chances are slim they will ever be. A detailed explanation is beyond the scope of this article so we will not be covering this.

For now, it’s time to roll up our sleeves as we start creating our very own Job System!

Creating a Design for the Job System

Before we start, we need to think about the design of the system for a moment. Simply put, we want to submit jobs to a manager that executes them for us. Next to that, we want to have more control over the execution order of each job. In the system that we’ll be developing we came up with the following design:

JobManagerThis is the class to which we can submit jobs to perform. Developers are able to queue jobs, run them immediately and control their execution order based on priority. The execution will all be handled by the JobManager.
JobThis class will actually be the job that we are executing and contains the specialized logic (such as a coroutine). It also contains information about the priority and naming of the job. The naming could be used for debugging purposes.
JobHandleTo facilitate an extra layer of abstraction between the JobManager and the Job classes, we’ll be creating a JobHandle class that will act as a mediator between them.

It also holds information about the status of the job, meaning whether it is scheduled, active or completed. The main reasoning behind this is that in our system we don’t want to expose methods that change the status of a job directly
Design of our Job System

Constants for the Job System

In order for the Job System to successfully apply a priority and a status to each job, we need to define a pair of constants. We implement these as a pair of Enums so they can be more easily expanded on later. Next to that, using enums makes it easier for developers to intuitively understand the code. Finally, for the JobPriority Enum we introduce a default value that corresponds to the Normal Priority. This value will be set as the default priority for each job.

  1. public enum JobPriority
  2. {
  3. Low = 0,
  4. Normal,
  5. Medium,
  6. High,
  7. Default = Normal
  8. }
  10. public enum JobStatus
  11. {
  12. None = 0,
  13. Scheduled,
  14. Active,
  15. Complete
  16. }

The JobHandle Class

Whenever a job is submitted to our Job System, the JobManager will create a new handle for it. Below you can find the code that implements the JobHandle class. As can be seen, it is pretty much a wrapper around our Job class. Also, it adds extra functionality in order to get and set the state of a job.

  1. public class JobHandle
  2. {
  3. private JobStatus m_Status = JobStatus.None;
  4. private Job m_Job = null;
  6. public JobHandle(Job job)
  7. {
  8. m_Job = job;
  9. }
  11. public IEnumerator RunJob()
  12. {
  13. return m_Job.Run();
  14. }
  16. public string GetIdentifier()
  17. {
  18. return m_Job.GetIdentifier();
  19. }
  21. public JobPriority GetPriority()
  22. {
  23. return m_Job.GetPriority();
  24. }
  26. public JobStatus GetStatus()
  27. {
  28. return m_Status;
  29. }
  31. public void ChangeStatus(JobStatus status)
  32. {
  33. m_Status = status;
  34. }
  35. }

The abstract Job Class

In the snipper below, you can find the definition of the abstract Job class. By making it abstract, we force developers to implement a specialized version of the class. As a result, the job has to be implemented before it can be submitted to the JobManager.

  1. public abstract class Job
  2. {
  3. private JobPriority m_Priority = JobPriority.Default;
  4. private string m_Identifier = string.Empty;
  5. private JobHandle m_Handle = null;
  7. public abstract IEnumerator Run();
  9. public Job SetPriority(JobPriority priority)
  10. {
  11. m_Priority = priority;
  12. return this;
  13. }
  15. public JobPriority GetPriority()
  16. {
  17. return m_Priority;
  18. }
  20. public Job SetIdentifier(string identifier)
  21. {
  22. m_Identifier = identifier;
  23. return this;
  24. }
  26. public string GetIdentifier()
  27. {
  28. return m_Identifier;
  29. }
  31. public void SetHandle(JobHandle handle)
  32. {
  33. m_Handle = handle;
  34. }
  36. public JobStatus GetStatus()
  37. {
  38. if(m_Handle == null)
  39. {
  40. return JobStatus.None;
  41. }
  42. else
  43. {
  44. return m_Handle.GetStatus();
  45. }
  46. }
  47. }

Another thing you may have noticed in the code is that there are several Setter methods which return the instance of the Job. This is done so it becomes easier for developers to initialize the job using chaining. Below you can find a snippet where we create a new ExampleJob and initialize it. During the initialization we set its identifier and priority values. More usage examples can be found at the end of this article.

  1. new ExampleJob()
  2. .SetIdentifier("The Best job")
  3. .SetPriority(JobPriority.Medium);

As mentioned before, to submit a job, a developer has to implement the abstract Run() method that actually will be run inside the coroutine. We will now show some examples of specialized jobs.

Specialized DelayJob Class

The first specialized job implementation we will be looking at is the DelayJob class. Even though this class does nothing particularly useful, it might give you more insight into what is actually going on in the code.

Now, if you have worked with coroutines before, you might recognize the WaitForSeconds() method exposed through the Unity API. Quite simply, the main purpose of this job is to let the Job Manager run a job for a specified duration. The job will be considered complete after the Run() method finishes execution.

  1. public class DelayJob : Job
  2. {
  3. private float m_Duration = 0.0f;
  5. public DelayJob(float duration)
  6. {
  7. m_Duration = duration;
  8. }
  10. public override IEnumerator Run()
  11. {
  12. yield return new WaitForSeconds(m_Duration);
  13. }
  14. }

Specialized WebRequestJob Class

In contrast, the WebRequestJob class provides a more practical example of what a job can be. In this case, we are performing a WebRequest to a specified address and return its response. The job will be considered complete once the created UnityWebRequestAsyncOperation has been completed and the response has been logged to the debug console..

  1. public class WebRequestJob : Job
  2. {
  3. private string m_Address = string.Empty;
  5. public WebRequestJob(string address)
  6. {
  7. m_Address = address;
  8. }
  10. public override IEnumerator Run()
  11. {
  12. UnityWebRequestAsyncOperation operation = UnityWebRequest.Get(m_Address).SendWebRequest();
  14. while(!operation.isDone)
  15. {
  16. yield return null;
  17. }
  19. Debug.Log(operation.webRequest.downloadHandler.text);
  20. }
  21. }

AnonymousJob Class

Finally, we have the AnonymousJob class which is our equivalent of an Anonymous Method in C#. Using this job, we can simply pass in a function (An IEnumerator in this case) through the constructor and use it as a coroutine. When ordered by the JobManager, the job will execute the given function as a coroutine.

  1. public class AnonymousJob : Job
  2. {
  3. private IEnumerator m_JobFunction = null;
  5. public AnonymousJob(IEnumerator jobFunction)
  6. {
  7. m_JobFunction = jobFunction;
  8. }
  10. public override IEnumerator Run()
  11. {
  12. yield return m_JobFunction;
  13. }
  14. }

The JobManager Class

Now, the JobManager is the most complex class in our system. As it’s name suggests, this is the actual class that will be managing our Coroutines inside Unity. Because of this, we will try to go into a little more detail to explain its inner workings. One important point to make is that we have implemented it as a UnityEngine.MonoBehaviour which gives us access to the Update() method. Using this method, we can schedule and check for jobs that have been completed over time.

Next to that, the class manages two lists. The ScheduledJobs list holds a collection of scheduled jobs whereas the ActiveJobs list holds the jobs that are currently being executed. You will also see that we have clamped the number of ‘concurrent’ jobs to 2. This means that at any given point, we have a maximum of 2 jobs running.

Whenever we queue a new job, we add it through a job handle to our ScheduledJobs list. Next, if there are currently less than 2 jobs active, it means we can move some jobs from the ScheduledJobs list and add them to the ActiveJobs list. Once jobs are in the active list, they can be executed until completion.

  1. public class JobManager : MonoBehaviour
  2. {
  3. private List<JobHandle> m_ScheduledJobs = new List<JobHandle>();
  4. private List<JobHandle> m_ActiveJobs = new List<JobHandle>();
  5. private int m_MaxJobs = 2;
  7. public void Queue(Job job)
  8. {
  9. JobHandle handle = new JobHandle(job);
  10. handle.ChangeStatus(JobStatus.Scheduled);
  12. m_ScheduledJobs.Add(handle);
  13. }
  15. public void Queue(IEnumerator jobFunction)
  16. {
  17. Queue(new AnonymousJob(jobFunction));
  18. }
  20. public void Run(Job job)
  21. {
  22. StartJob(new JobHandle(job));
  23. }
  25. public void Run(IEnumerator jobFunction)
  26. {
  27. Run(new AnonymousJob(jobFunction));
  28. }
  30. public void Update()
  31. {
  32. ExecuteJobs();
  33. }
  35. public int GetActiveJobCount()
  36. {
  37. return m_ActiveJobs.Count;
  38. }
  40. public bool HasActiveJobs()
  41. {
  42. return GetActiveJobCount() > 0;
  43. }
  45. protected IEnumerator BaseJobFunction(JobHandle jobHandle)
  46. {
  47. m_ActiveJobs.Add(jobHandle);
  48. jobHandle.ChangeStatus(JobStatus.Active);
  50. Debug.LogFormat("Starting job '{0}'...", jobHandle.GetIdentifier());
  51. yield return jobHandle.RunJob();
  52. Debug.LogFormat("Completed job '{0}'", jobHandle.GetIdentifier());
  54. m_ActiveJobs.Remove(jobHandle);
  55. jobHandle.ChangeStatus(JobStatus.Complete);
  56. }
  58. private void StartJob(JobHandle handle)
  59. {
  60. StartCoroutine(BaseJobFunction(handle));
  61. }
  63. private void ExecuteJobs()
  64. {
  65. if(m_ActiveJobs.Count < m_MaxJobs)
  66. {
  67. if(m_ScheduledJobs.Count > 0)
  68. {
  69. m_ScheduledJobs = m_ScheduledJobs.OrderByDescending(jobHandle => jobHandle.GetPriority()).ToList();
  71. JobHandle firstJob = m_ScheduledJobs.First();
  73. StartJob(firstJob);
  75. m_ScheduledJobs.Remove(firstJob);
  76. }
  77. }
  78. }
  79. }

Also, our JobManager class features a BaseJobFunction() that acts as a coroutine in which a job coroutine will run. This is useful because it allows us to set the state of the job outside the job itself. Without it, we would need to add unnecessary implementation details to the Job classes themselves.

Finally, we want to cover the priority sorting which we do every time the system detects that it can schedule a new job. Using the OrderByDescending() method from System.Linq, we are able to update the list of scheduled jobs based on their priority. Important to note is that we could also do this every time we queue a new job. However, in our implementation it is possible to change the priority of the job after it has been scheduled. This is useful as it allows more flexibility in reordering of the jobs if needed.

Example of using the Coroutine Job System

Now that every element has been implemented correctly, we can start using our new system. In the snippet below you can find a case where we schedule 3 jobs and try executing 1 job immediately.

When running this, you will find that the Unity3D Job will run first. This job is then followed by the Delay Job, the Google Job and finally the Microsoft Job. This is because the Unity3D Job is forced to run and the Delay Job has the highest priority. After that, the JobManager will simply execute the jobs in the order they have been submitted. As you can see, managing coroutines in Unity becomes very easy!

  1. public class JobExamples : MonoBehaviour
  2. {
  3. [SerializeField]
  4. private JobManager m_JobManager = null;
  6. private void Awake()
  7. {
  8. m_JobManager.Queue(new WebRequestJob("")
  9. .SetIdentifier("Google Job"));
  11. m_JobManager.Queue(new DelayJob(3.0f)
  12. .SetIdentifier("Delay 3.0s")
  13. .SetPriority(JobPriority.High));
  15. m_JobManager.Queue(new WebRequestJob("")
  16. .SetIdentifier("Microsoft Job"));
  18. m_JobManager.Run(new WebRequestJob("")
  19. .SetIdentifier("Unity3D Job"));
  20. }
  21. }

Another way to submit jobs to the system is by using the Anonymous overload that we created earlier. Rather than creating a specialized class implementation for a job, we can simply put in any coroutine that we want to execute. In the sample below, you can see how this is done:

  1. public class JobExamples : MonoBehaviour
  2. {
  3. [SerializeField]
  4. private JobManager m_JobManager = null;
  5. [SerializeField]
  6. private GameObject m_SpawnedObject = null;
  8. private void Awake()
  9. {
  10. m_JobManager.Queue(SpawnDelayed(2.0f, m_SpawnedObject));
  11. }
  13. private IEnumerator SpawnDelayed(float delay, GameObject original)
  14. {
  15. yield return new WaitForSeconds(delay);
  16. GameObject.Instantiate(original);
  17. }
  18. }


What you learned in this article:

  • In Unity, coroutines allow you to create methods that execute over time.
  • A Job System is a system that processes little tasks.simultaneously.
  • Tasks in a Job System are referred to as Jobs and can be executed in a predetermined order.
  • In the Unity Job system, coroutines are not supported and chances are slim they will ever be.
  • We shared in implementation of a job system used in managing coroutines in Unity


To conclude, we have now implemented a job system particularly aimed for managing coroutines in Unity. More importantly, we have created a system that can be scaled out with more specialized jobs.. Another benefit is that each job adheres to the Single Object Responsibility rule often found in Object Oriented Programming. Over time, more features can be added, such as dependencies between different jobs by implementing a more complex sorting algorithm.

Further Reading