I’ve always thought that only the HPC# (High Performance C#) subset can be executed in jobs which are run in Unity’s reserved worker threads. We do some multithreading in our game but we’re running them in C#’s managed threads as they’re not using HPC#. I feel like I’m wasting the worker threads if they’re not fully utilized. While we have ECS systems running in worker threads, they’re using very little running time.

As much as I want to utilize the worker threads, it’s very hard to port a huge existing OOP code to HPC# so that I can use the job system. I would have to rewrite the game from scratch which is stupid. That is until I read a thread from the Unity forums on how you can run managed C# code in jobs. I tried it on a simple test and I’m surprised how easy it is.

Simple Example

Here’s an example to run managed code in a job:

// Common interface for executing something interface Task { void Execute(); } // Just an arbitrary heavy computation as an example class SampleHeavyTask : Task { private Unity.Mathematics.Random random; public SampleHeavyTask() { this.random = new Unity.Mathematics.Random(1234); } public void Execute() { float total = 0; for (int i = 0; i < 50000; ++i) { float randomValue = this.random.NextFloat(1, 100); total += math.sqrt(randomValue); } } } public class ManagedCodeInJob : MonoBehaviour { private readonly List<GCHandle> gcHandles = new List<GCHandle>(); private readonly List<JobHandle> jobHandles = new List<JobHandle>(); private void Update() { // Schedule one task per frame ScheduleTask(); } private void ScheduleTask() { Task task = new SampleHeavyTask(); GCHandle gcHandle = GCHandle.Alloc(task); this.gcHandles.Add(gcHandle); // We remember this so we can free it later Job job = new Job() { handle = gcHandle }; // We remember the JobHandle so we can complete it later this.jobHandles.Add(job.Schedule()); } private void LateUpdate() { // Free and complete the scheduled jobs for (int i = 0; i < this.jobHandles.Count; ++i) { this.jobHandles[i].Complete(); this.gcHandles[i].Free(); } this.jobHandles.Clear(); this.gcHandles.Clear(); } private struct Job : IJob { public GCHandle handle; public void Execute() { Task task = (Task) handle.Target; task.Execute(); } } }

The key here is the Job struct code found at the bottom. You provide it with a GCHandle that targets an instance that provides the execution. You can just cast it then call the method that executes your task.

In Update(), we schedule one job which tells Unity "Hey, please run this in a worker thread if you can" (sometimes Unity can't). Then in LateUpdate(), we force that job to complete which means that main thread will wait for the task execution that's in the worker thread to be completed. We also call GCHandle.Free().

I admit that I didn't know what GCHandle does until the writing of this post. I'll explain as best as I can. GCHandle.Alloc() protects a managed object from being garbage collected. I would surmise that the worker threads are unmanaged/native environments which is outside the jurisdiction of the garbage collector. We have to make sure that the garbage collector does not collect the objects while the worker threads are still executing them. GCHandle.Alloc() kind of removes an object from the garbage collector's radar. After the thread execution completes, we have to call GCHandle.Free() to mark the object as garbage collectable again.

If you attach the script above to a GameObject and run the scene, you will see something like this in the profiler:

You can see that the job that executes the sample heavy task is run in one of the worker threads.

My PC has 4 cores, so Unity automatically creates 3 worker threads (coreCount – 1). The remaining one will be the main thread. Let’s look at what happens when we call ScheduleTask() in Update() three times:

private void Update() { // Of course you can use a for loop here. ScheduleTask(); ScheduleTask(); ScheduleTask(); }

This is what the profiler looks like in my machine:

We can see here that the three worker threads are each executing the heavy job in parallel. In the main thread, you can see that LateUpdate() only waits for 5.57ms instead of waiting the 16.45ms total execution time of the three worker threads. That’s multithreading at work.

Wait for execution to finish

The current example forces the threads to finish before the frame ends. This is done by calling JobHandle.Complete() in LateUpdate(). However, there may be times where you want to wait for the thread execution to finish instead of forcing it to complete. This implies that the thread may execute the job in multiple frames. This is useful for tasks that may run more than 16ms per execution.

To do this, we can simply change LateUpdate() to the following:

private void LateUpdate() { // Wait for jobs to complete before freeing and removing them // Iterate from the end so we can easily remove from lists for (int i = this.jobHandles.Count - 1; i >= 0; --i) { if (this.jobHandles[i].IsCompleted) { this.jobHandles[i].Complete(); this.jobHandles.RemoveAt(i); this.gcHandles[i].Free(); this.gcHandles.RemoveAt(i); } } }

Be careful!

As easy as this is, this is not without drawbacks. The normal job system using HPC# would usually tell you what you’re doing wrong in terms of doing multithreading code such as race conditions.

In using managed code, you’re on your own. Unity won’t tell you anything so you have to make sure that your multithreaded code is correct. Multithreading is inherently hard so be careful. Use this only if you know what you’re doing.

That’s all for now. Hope it helps!