Multithreading with Job System in Unity

Writing multithreaded code was never something easy, but this might change with the introduction of Job System in Unity.

If you didn’t know, the code that you write in Unity is executed on the main thread. This makes everything much easier to build, but it also has some drawbacks, mainly in the performance department.

I mentioned in the beginning, Unity is introducing Job System to help you write multithreaded code much easier. And the best part is that you don’t have to worry about race conditions! 🥰

However, you still have to know what are you doing. 😉

What is Multithreading?

Multithreading, as a name suggests, is a way of writing code that runs on more than one thread. As we are living in a time where all of the CPUs have multiple cores and threads, this gives us more power to do operations and run your code.

A multithreaded code allows you to run it in parallel and helps a lot with performance, especially when you have to do a lot of calculations. But you don’t often have to write multithreaded code. And maybe it’s for the better. 😅

Writing such code requires a lot of knowledge and understanding, which prevents you from making mistakes that led to race conditions, deadlocking, and more. Believe me, it’s irritating to debug such code… 😩

I’m not going to mention that writing multithreaded code for Unity was double pain. It required you to sync output from other threads with the main thread to use it.

But you shouldn’t care about it anymore! We have a Job System!

Job System

Job System is part of a new Data-Oriented Technology Stack which also includes ECS and Burst Compiler.

It was designed to help developers write thread-safe code and take advantage of the ECS architecture.

However, Job System doesn’t create threads! It creates jobs which are small units of highly specific work on one thing.

ECS code vs Job System code

‼️ Disclaimer: I’m using examples from the Unity ECS Sample repository. ‼️

So if Job System works well with the ECS, there should be similarities in code, right?

Let’s see how ECS code looks like!

First, we have a component.

using System;
using Unity.Entities;

// Serializable attribute is for editor support.
// ReSharper disable once InconsistentNaming
[Serializable]
public struct RotationSpeed_ForEach : IComponentData
{
    public float RadiansPerSecond;
}

And a system.

using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;

// This system updates all entities in the scene with both a RotationSpeed_ForEach and Rotation component.

// ReSharper disable once InconsistentNaming
public class RotationSpeedSystem_ForEach : ComponentSystem
{
    protected override void OnUpdate()
    {
        // Entities.ForEach processes each set of ComponentData on the main thread. This is not the recommended
        // method for best performance. However, we start with it here to demonstrate the clearer separation
        // between ComponentSystem Update (logic) and ComponentData (data).
        // There is no update logic on the individual ComponentData.
        Entities.ForEach((ref RotationSpeed_ForEach rotationSpeed, ref Rotation rotation) =>
        {
            var deltaTime = Time.deltaTime;
            rotation.Value = math.mul(math.normalize(rotation.Value),
                quaternion.AxisAngle(math.up(), rotationSpeed.RadiansPerSecond * deltaTime));
        });
    }
}

Easy! A component is a struct which holds data which is later processed by a system.

Now let’s see Job System code!

Again we have a component, which is the same.

using System;
using Unity.Entities;

// ReSharper disable once InconsistentNaming
[Serializable]
public struct RotationSpeed_IJobForEach : IComponentData
{
    public float RadiansPerSecond;
}

But Job Component System is a little different.

using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;

// This system updates all entities in the scene with both a RotationSpeed_IJobForEach and Rotation component.

// ReSharper disable once InconsistentNaming
public class RotationSpeedSystem_IJobForEach : JobComponentSystem
{
    // Use the [BurstCompile] attribute to compile a job with Burst. You may see significant speed ups, so try it!
    [BurstCompile]
    struct RotationSpeedJob : IJobForEach<Rotation, RotationSpeed_IJobForEach>
    {
        public float DeltaTime;

        // The [ReadOnly] attribute tells the job scheduler that this job will not write to rotSpeedIJobForEach
        public void Execute(ref Rotation rotation, [ReadOnly] ref RotationSpeed_IJobForEach rotSpeedIJobForEach)
        {
            // Rotate something about its up vector at the speed given by RotationSpeed_IJobForEach.
            rotation.Value = math.mul(math.normalize(rotation.Value), quaternion.AxisAngle(math.up(), rotSpeedIJobForEach.RadiansPerSecond * DeltaTime));
        }
    }

    // OnUpdate runs on the main thread.
    protected override JobHandle OnUpdate(JobHandle inputDependencies)
    {
        var job = new RotationSpeedJob
        {
            DeltaTime = Time.deltaTime
        };

        return job.Schedule(this, inputDependencies);
    }
}

You can see that Job System splits the ECS Update method even further! So in the Update method, we are scheduling jobs in which we are executing ECS system logic. There is also little catch.

Jobs, like components, are structs with data needed to execute them. They also implement method Execute to run the job.

In this sample, you can spot that this Job has a DeltaTime variable. It is necessary to save Time.deltaTime to job variable because we will be running this code in parallel, as we shouldn’t access data from outside.

Burst Compiler

You can also see that Job in this sample has BurstCompile attribute. This simple addition can boost your code even further without doing almost anything!

Burst Compiler is a compiler that produces highly optimized machine-code for a platform that you are compiling for.

And you just need to do 2 things! Add BurstCompiler attribute to your jobs and enable in Jobs > Burst > Enable Compilation.

Enabling BurstCompiler in the project.

Great! That was a little lengthy introduction… 😅

Have you ever tried to write multithreaded code? Let me know in the comment section below!

If you want to get notified on future content, sign up for the newsletter!

This time I used a sample from the official Unity repository. 🔗

And I hope to see you next time! 🤓

avatar
  Subscribe  
Notify of