There’s been a lot of buzz surrounding the Unity Job System over the past couple of years. But a lot of Unity developers still don’t seem to understand what it is or how it can benefit them.
In this post, I’m going to shed a little light on what the Unity Job System is, how it can help you, and what it looks like in practice.
Unity Capsicum: Performance by Default
Unity 2018 came with a new set of features that introduced the concept of performance by default. Code named “Capsicum”, this feature set includes the Unity Job System, Unity ECS, and the Burst Compiler.
When used correctly, Capsicum dramatically improves the performance of your game code.
But this post is about the Unity Job System. So, what is it? Well, at a high level, the Unity Job System allows you to introduce multithreading to your game code.
Improving Performance With Multithreading
Multithreading is a type of programming that takes advantage of a CPU’s capacity to process many threads at the same time across multiple cores.
A thread contains all of the contextual data needed to execute a set of instructions.
Traditionally, applications run all of their code on a single thread, called the “main thread”. But with multithreading you can write applications that execute multiple sets of instructions concurrently, or at the same time.
How Does Multithreading Improve Performance?
Multithreading is a key ingredient in improving the performance of your game code.
By splitting up CPU intensive operations into multiple processes that can all run at the same time, you can dramatically reduce how long those operations takes to execute. This results in massive improvements to loading times, frame rates, and battery life.
So, why do a lot of developers seem to avoid multithreaded code like the plague?
Why is it so Hard to Write Multithreaded Code?
Multithreaded code is complex and error-prone. For one, it’s easy to let your thread count get out of control, which can cause frequent context switching.
A Context Switch is a procedure that the CPU follows to change from one task to another. This happens automatically and it plays an important role in your computer’s ability to multitask.
During a context switch, the CPU has to store the state, or context, of the running task. This information is important because it helps the CPU avoid conflicts and also to resume the task later.
The problem is that context switches are expensive. And the time it takes to perform them adds up quickly. This becomes more of a problem as the number of active threads approaches and exceeds the number of available cores.
Another complication of multithreaded code is the possibility of race conditions.
A race condition is a situation where your code behaves differently depending on the order in which it executes.
You can start threads in any order you want, but there’s no way to tell how they’ll run in relation to one another or when each one will finish finish. This indeterminism can result in unpredictable behavior and errors that are hard to debug.
This is especially true if two or more threads share and modify the same data.
Imagine a room with multiple light switches that all affect the same light bulb. If multiple people enter the room and randomly toggle light switches throughout the day, then it’s hard to predict whether the light will be on or off at any given moment.
On top of these examples, there are plenty of other factors that make writing multithreaded code a complex and difficult task. And that is where the Job System comes in.
The Unity Job System
The Unity Job System allows you to write multithreaded code easily and safely. It does this by creating jobs instead of threads.
A Job represents a units of work that the system can process as a series of steps.
When you a schedule job, the system places it into a special queue. Then, at some point during execution, a worker thread will pull the job out of the queue and execute it.
Worker threads are individual threads that are managed by the Unity Job. They complete jobs in the background so they don’t interrupt the main thread.
All you need to do is place your logic into custom jobs and then schedule them to run. The Job System will handle the rest, executing all of your jobs on seperate threads that are managed by Unity.
Why not Write Your own Multithreaded Code?
So how is this better than writing your own multithreaded code? I mean, under the covers, the Job System is still creating threads right? It’s still susceptible to all of the difficulties associated with multithreading.
The difference is that the developers who created the Unity Job System have gone to great lengths to ensure that their multithreaded code is iron clad.
For instance, the Job System will do it’s best to avoid context switching by only creating one worker thread per logical CPU core. This allows you to create as many jobs as you want (within reason) without having to worry about how it’ll affect the performance of your CPU.
The Job System also has a built-in mechanism for guarding against race conditions in the form of job dependencies. For example, if Job A needs to prepare some data for Job B, you can assign it as Job B’s dependency. That way Job A will always run first, and Job B will always have the correct data.
So what does a job look like and how do you schedule one? Let’s take a quick look at an example from Stalla3D’s Job System Cookbook Github Repo.
Using the Unity Job System to Modify a Mesh
In this example, the job uses Perlin Noise to modify the vertices and normals of a mesh during each frame. The job takes an array of vertices and normals, and floats that represent sine time, cosine time, and strength.
It’s execute method runs for each vertex in the mesh, and its corresponding normal. All it really does is calculate a new vertex and normal for the given index. And this is where the power of the Job System lies.
When this job is scheduled, Unity will queue up as many instances of it as it needs and the Worker threads will begin to execute them in parallel. The more logical cores your CPU has, the less time it’ll take for this job to complete.
I’ll go over an example in more detail in another post, but you can see by the Update method that scheduling this job is extremely simple. And the overall code footprint is much easier to understand and safer to write than low level multithreaded code would be.
So that’s the Unity Job System at a high level. It allows you to use multithreading in a way that safe, easy, and completely managed by Unity.
And when you combine it with Unity ECS and the Burst Compiler, you get extremely performant code that’s optimized by default.