Job System Tutorial
The new Job System debuted recently in Unity 2018.1 and began the process of changing how virtually all Unity scripts will be written. In conjunction with the forthcoming ECS and Burst compiler, the old MonoBehaviour
-based programming paradigm will eventually be replaced. Today’s article is a tutorial for how to get started learning the new way of writing Unity scripts.
Update: A Russian translation of this article is available.
Usable Types
Any blittable value type can be used in any job. For a type to be both a blittable type and a value type, it must be a primitive (e.g. int
), an enum, a pointer, or a struct containing only primitives, enums, pointers, and blittable structs.
This definition excludes all reference types. This means that classes, interfaces, delegates, dynamic types, strings, and (managed) arrays can’t be used by jobs.
Native collections like NativeArray<T>
and NativeSlice<T>
can be used by jobs. Custom native collection types can also be used. Many more types like NativeHashMap<TKey, TValue>
will become available in future versions of Unity, especially with the ECS (Entity-Component-System) that is currently in “preview.”
Usable APIs
Since jobs run off of the main thread, they can’t access most of the Unity API. Only specific parts of the Unity API can be used. To my knowledge, the only documented non-experimental API that’s safe to use as of 2018.1 is Transform
and it must be accessed via a TransformAccess
and TransformAccessArray
. We’ll see how to do this below.
Aside from this one documented API, we can find more functionality that can be used off the main thread by reading through Unity’s C# source. Search for either the [ThreadSafe] attribute or the [FreeFunction] attribute with IsThreadSafe
set to true
to find quite a few usable functions.
As of 2018.1.0f2, here are all the thread-safe functions:
UnityEngine.CoreModule/UnityEngine.iOS/Device.cs- public static extern void SetNoBackupFlag(string path); UnityEngine.CoreModule/UnityEngine.iOS/Device.cs- public static extern void ResetNoBackupFlag(string path); UnityEngine.CoreModule/UnityEngine.iOS/Device.cs- public static extern bool RequestStoreReview(); UnityEngine.CoreModule/UnityEngine.iOS/NotificationHelper.cs- internal static extern void DestroyLocal(IntPtr target); UnityEngine.CoreModule/UnityEngine.iOS/NotificationHelper.cs- internal static extern void DestroyRemote(IntPtr target); UnityEngine.CoreModule/UnityEngine.iOS/OnDemandResourcesRequest.cs- private static extern void DestroyFromScript(IntPtr ptr); UnityEngine.CoreModule/UnityEngine/Matrix4x4.cs- private Quaternion GetRotation() UnityEngine.CoreModule/UnityEngine/Matrix4x4.cs- private Vector3 GetLossyScale() UnityEngine.CoreModule/UnityEngine/Matrix4x4.cs- private bool IsIdentity() UnityEngine.CoreModule/UnityEngine/Matrix4x4.cs- private float GetDeterminant() UnityEngine.CoreModule/UnityEngine/Matrix4x4.cs- private FrustumPlanes DecomposeProjection() UnityEngine.CoreModule/UnityEngine/Matrix4x4.cs- public bool ValidTRS() UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/AtomicSafetyHandle.cs- public static AtomicSafetyHandle Create() UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/AtomicSafetyHandle.cs- public static AtomicSafetyHandle GetTempUnsafePtrSliceHandle() UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/AtomicSafetyHandle.cs- public static void Release(AtomicSafetyHandle handle) UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/AtomicSafetyHandle.cs- public static extern void UseSecondaryVersion(ref AtomicSafetyHandle handle); UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/AtomicSafetyHandle.cs- public static void SetAllowSecondaryVersionWriting(AtomicSafetyHandle handle, bool allowWriting) UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/AtomicSafetyHandle.cs- public static EnforceJobResult EnforceAllBufferJobsHaveCompleted(AtomicSafetyHandle handle) UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/AtomicSafetyHandle.cs- public static EnforceJobResult EnforceAllBufferJobsHaveCompletedAndRelease(AtomicSafetyHandle handle) UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/AtomicSafetyHandle.cs- public static EnforceJobResult EnforceAllBufferJobsHaveCompletedAndDisableReadWrite(AtomicSafetyHandle handle) UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/AtomicSafetyHandle.cs- public static int GetReaderArray(AtomicSafetyHandle handle, int maxCount, IntPtr output) UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/AtomicSafetyHandle.cs- public static JobHandle GetWriter(AtomicSafetyHandle handle) UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/AtomicSafetyHandle.cs- public static string GetReaderName(AtomicSafetyHandle handle, int readerIndex) UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/AtomicSafetyHandle.cs- public static string GetWriterName(AtomicSafetyHandle handle) UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/UnsafeUtility.cs- private static extern int GetFieldOffsetInStruct(FieldInfo field); UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/UnsafeUtility.cs- private static extern int GetFieldOffsetInClass(FieldInfo field); UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/UnsafeUtility.cs- public unsafe static extern void* PinGCObjectAndGetAddress(object target, out ulong gcHandle); UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/UnsafeUtility.cs- public static extern void ReleaseGCObject(ulong gcHandle); UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/UnsafeUtility.cs- public unsafe static extern void CopyObjectAddressToPtr(object target, void* dstPtr); UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/UnsafeUtility.cs- public unsafe static extern void* Malloc(long size, int alignment, Allocator allocator); UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/UnsafeUtility.cs- public unsafe static extern void Free(void* memory, Allocator allocator); UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/UnsafeUtility.cs- public unsafe static extern void MemCpy(void* destination, void* source, long size); UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/UnsafeUtility.cs- public unsafe static extern void MemCpyReplicate(void* destination, void* source, int size, int count); UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/UnsafeUtility.cs- public unsafe static extern void MemCpyStride(void* destination, int destinationStride, void* source, int sourceStride, int elementSize, int count); UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/UnsafeUtility.cs- public unsafe static extern void MemMove(void* destination, void* source, long size); UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/UnsafeUtility.cs- public unsafe static extern void MemClear(void* destination, long size); UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/UnsafeUtility.cs- public static extern int SizeOf(Type type); UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/UnsafeUtility.cs- public static extern bool IsBlittable(Type type); UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/UnsafeUtility.cs- internal static extern void LogError(string msg, string filename, int linenumber); UnityEngine.CoreModule/UnityEngine/Bounds.cs- public bool Contains(Vector3 point) UnityEngine.CoreModule/UnityEngine/Bounds.cs- public float SqrDistance(Vector3 point) UnityEngine.CoreModule/UnityEngine/Bounds.cs- private static bool IntersectRayAABB(Ray ray, Bounds bounds, out float dist) UnityEngine.CoreModule/UnityEngine/Bounds.cs- public Vector3 ClosestPoint(Vector3 point) UnityEngine.CoreModule/UnityEngine/MaterialPropertyBlock.cs- private static extern void DestroyImpl(IntPtr mpb); UnityEngine.CoreModule/UnityEngine/Mathf.cs- public static extern int ClosestPowerOfTwo(int value); UnityEngine.CoreModule/UnityEngine/Mathf.cs- public static extern bool IsPowerOfTwo(int value); UnityEngine.CoreModule/UnityEngine/Mathf.cs- public static extern int NextPowerOfTwo(int value); UnityEngine.CoreModule/UnityEngine/Mathf.cs- public static extern float GammaToLinearSpace(float value); UnityEngine.CoreModule/UnityEngine/Mathf.cs- public static extern float LinearToGammaSpace(float value); UnityEngine.CoreModule/UnityEngine/Mathf.cs- public static Color CorrelatedColorTemperatureToRGB(float kelvin) UnityEngine.CoreModule/UnityEngine/Mathf.cs- public static extern ushort FloatToHalf(float val); UnityEngine.CoreModule/UnityEngine/Mathf.cs- public static extern float HalfToFloat(ushort val); UnityEngine.CoreModule/UnityEngine/Mathf.cs- public static extern float PerlinNoise(float x, float y); UnityEngine.CoreModule/UnityEngine/Matrix4x4.cs- public static Matrix4x4 TRS(Vector3 pos, Quaternion q, Vector3 s) UnityEngine.CoreModule/UnityEngine/Matrix4x4.cs- public static Matrix4x4 Inverse(Matrix4x4 m) UnityEngine.CoreModule/UnityEngine/Matrix4x4.cs- public static Matrix4x4 Transpose(Matrix4x4 m) UnityEngine.CoreModule/UnityEngine/Matrix4x4.cs- public static Matrix4x4 Ortho(float left, float right, float bottom, float top, float zNear, float zFar) UnityEngine.CoreModule/UnityEngine/Matrix4x4.cs- public static Matrix4x4 Perspective(float fov, float aspect, float zNear, float zFar) UnityEngine.CoreModule/UnityEngine/Matrix4x4.cs- public static Matrix4x4 LookAt(Vector3 from, Vector3 to, Vector3 up) UnityEngine.CoreModule/UnityEngine/Matrix4x4.cs- public static Matrix4x4 Frustum(float left, float right, float bottom, float top, float zNear, float zFar) UnityEngine.CoreModule/UnityEngine/MonoBehaviour.cs- private static extern void ConstructorCheck([Writable] Object self); UnityEngine.CoreModule/UnityEngine/Object.cs- private static extern int GetOffsetOfInstanceIDInCPlusPlusObject(); UnityEngine.CoreModule/UnityEngine/Object.cs- private static extern bool CurrentThreadIsMainThread(); UnityEngine.CoreModule/UnityEngine/Object.cs- internal static extern bool DoesObjectWithInstanceIDExist(int instanceID); UnityEngine.CoreModule/UnityEngine/Ping.cs- private static extern void Internal_Destroy(IntPtr ptr); UnityEngine.CoreModule/UnityEngine/Quaternion.cs- public static Quaternion FromToRotation(Vector3 fromDirection, Vector3 toDirection) UnityEngine.CoreModule/UnityEngine/Quaternion.cs- public static Quaternion Inverse(Quaternion rotation) UnityEngine.CoreModule/UnityEngine/Quaternion.cs- public static Quaternion Slerp(Quaternion a, Quaternion b, float t) UnityEngine.CoreModule/UnityEngine/Quaternion.cs- public static Quaternion SlerpUnclamped(Quaternion a, Quaternion b, float t) UnityEngine.CoreModule/UnityEngine/Quaternion.cs- public static Quaternion Lerp(Quaternion a, Quaternion b, float t) UnityEngine.CoreModule/UnityEngine/Quaternion.cs- public static Quaternion LerpUnclamped(Quaternion a, Quaternion b, float t) UnityEngine.CoreModule/UnityEngine/Quaternion.cs- private static Quaternion Internal_FromEulerRad(Vector3 euler) UnityEngine.CoreModule/UnityEngine/Quaternion.cs- private static Vector3 Internal_ToEulerRad(Quaternion rotation) UnityEngine.CoreModule/UnityEngine/Quaternion.cs- private static void Internal_ToAxisAngleRad(Quaternion q, out Vector3 axis, out float angle) UnityEngine.CoreModule/UnityEngine/Quaternion.cs- public static Quaternion AngleAxis(float angle, Vector3 axis) UnityEngine.CoreModule/UnityEngine/Quaternion.cs- public static Quaternion LookRotation(Vector3 forward, [DefaultValue("Vector3.up")] Vector3 upwards) UnityEngine.CoreModule/UnityEngine/Screen.cs- Width { get; } UnityEngine.CoreModule/UnityEngine/Screen.cs- Height { get; } UnityEngine.CoreModule/UnityEngine/ScriptableObject.cs- private static extern void CreateScriptableObject([Writable] ScriptableObject self); UnityEngine.CoreModule/UnityEngine/UnityLogWriter.cs- private static extern void WriteStringToUnityLogImpl(string s); UnityEngine.CoreModule/UnityEngine/Vector3.cs- public static Vector3 Slerp(Vector3 a, Vector3 b, float t) UnityEngine.CoreModule/UnityEngine/Vector3.cs- public static Vector3 SlerpUnclamped(Vector3 a, Vector3 b, float t) UnityEngine.CoreModule/UnityEngine/Vector3.cs- private static extern void OrthoNormalize2(ref Vector3 a, ref Vector3 b); UnityEngine.CoreModule/UnityEngine/Vector3.cs- private static extern void OrthoNormalize3(ref Vector3 a, ref Vector3 b, ref Vector3 c); UnityEngine.CoreModule/UnityEngine/Vector3.cs- public static Vector3 RotateTowards(Vector3 current, Vector3 target, float maxRadiansDelta, float maxMagnitudeDelta) UnityEngine.CoreModule/UnityEngine.Jobs/TransformAccessArray.cs- internal static extern IntPtr GetSortedTransformAccess(IntPtr transformArrayIntPtr); UnityEngine.CoreModule/UnityEngine.Jobs/TransformAccessArray.cs- internal static extern IntPtr GetSortedToUserIndex(IntPtr transformArrayIntPtr); UnityEngine.CoreModule/UnityEngine.Jobs/TransformAccess.cs- private static extern void GetPosition(ref TransformAccess access, out Vector3 p); UnityEngine.CoreModule/UnityEngine.Jobs/TransformAccess.cs- private static extern void SetPosition(ref TransformAccess access, ref Vector3 p); UnityEngine.CoreModule/UnityEngine.Jobs/TransformAccess.cs- private static extern void GetRotation(ref TransformAccess access, out Quaternion r); UnityEngine.CoreModule/UnityEngine.Jobs/TransformAccess.cs- private static extern void SetRotation(ref TransformAccess access, ref Quaternion r); UnityEngine.CoreModule/UnityEngine.Jobs/TransformAccess.cs- private static extern void GetLocalPosition(ref TransformAccess access, out Vector3 p); UnityEngine.CoreModule/UnityEngine.Jobs/TransformAccess.cs- private static extern void SetLocalPosition(ref TransformAccess access, ref Vector3 p); UnityEngine.CoreModule/UnityEngine.Jobs/TransformAccess.cs- private static extern void GetLocalRotation(ref TransformAccess access, out Quaternion r); UnityEngine.CoreModule/UnityEngine.Jobs/TransformAccess.cs- private static extern void SetLocalRotation(ref TransformAccess access, ref Quaternion r); UnityEngine.CoreModule/UnityEngine.Jobs/TransformAccess.cs- private static extern void GetLocalScale(ref TransformAccess access, out Vector3 r); UnityEngine.CoreModule/UnityEngine.Jobs/TransformAccess.cs- private static extern void SetLocalScale(ref TransformAccess access, ref Vector3 r); UnityEngine.CoreModule/Unity.Jobs.LowLevel.Unsafe/JobsUtility.cs- public static extern bool GetWorkStealingRange(ref JobRanges ranges, int jobIndex, out int beginIndex, out int endIndex); UnityEngine.CoreModule/Unity.Jobs.LowLevel.Unsafe/JobsUtility.cs- public unsafe static extern void PatchBufferMinMaxRanges(IntPtr bufferRangePatchData, void* jobdata, int startIndex, int rangeSize); UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/AtomicSafetyHandle.cs- public static void SetAllowReadOrWriteAccess(AtomicSafetyHandle handle, bool allowReadWriteAccess) UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/AtomicSafetyHandle.cs- public static bool GetAllowReadOrWriteAccess(AtomicSafetyHandle handle) UnityEngine.AIModule/UnityEngine.Experimental.AI/NavMeshQuery.cs- private static extern bool HasNodePool(IntPtr navMeshQuery); UnityEngine.AIModule/UnityEngine.Experimental.AI/NavMeshQuery.cs- private unsafe static PathQueryStatus BeginFindPath(IntPtr navMeshQuery, NavMeshLocation start, NavMeshLocation end, int areaMask, void* costs) UnityEngine.AIModule/UnityEngine.Experimental.AI/NavMeshQuery.cs- private static extern PathQueryStatus UpdateFindPath(IntPtr navMeshQuery, int iterations, out int iterationsPerformed); UnityEngine.AIModule/UnityEngine.Experimental.AI/NavMeshQuery.cs- private static extern PathQueryStatus EndFindPath(IntPtr navMeshQuery, out int pathSize); UnityEngine.AIModule/UnityEngine.Experimental.AI/NavMeshQuery.cs- private unsafe static extern int GetPathResult(IntPtr navMeshQuery, void* path, int maxPath); UnityEngine.AIModule/UnityEngine.Experimental.AI/NavMeshQuery.cs- private static bool IsValidPolygon(IntPtr navMeshQuery, PolygonId polygon) UnityEngine.AIModule/UnityEngine.Experimental.AI/NavMeshQuery.cs- private static int GetAgentTypeIdForPolygon(IntPtr navMeshQuery, PolygonId polygon) UnityEngine.AIModule/UnityEngine.Experimental.AI/NavMeshQuery.cs- private static bool IsPositionInPolygon(IntPtr navMeshQuery, Vector3 position, PolygonId polygon) UnityEngine.AIModule/UnityEngine.Experimental.AI/NavMeshQuery.cs- private static PathQueryStatus GetClosestPointOnPoly(IntPtr navMeshQuery, PolygonId polygon, Vector3 position, out Vector3 nearest) UnityEngine.AIModule/UnityEngine.Experimental.AI/NavMeshQuery.cs- private static NavMeshLocation MapLocation(IntPtr navMeshQuery, Vector3 position, Vector3 extents, int agentTypeID, int areaMask = -1) UnityEngine.AIModule/UnityEngine.Experimental.AI/NavMeshQuery.cs- private unsafe static extern void MoveLocations(IntPtr navMeshQuery, void* locations, void* targets, void* areaMasks, int count); UnityEngine.AIModule/UnityEngine.Experimental.AI/NavMeshQuery.cs- private unsafe static extern void MoveLocationsInSameAreas(IntPtr navMeshQuery, void* locations, void* targets, int count, int areaMask); UnityEngine.AIModule/UnityEngine.Experimental.AI/NavMeshQuery.cs- private static NavMeshLocation MoveLocation(IntPtr navMeshQuery, NavMeshLocation location, Vector3 target, int areaMask) UnityEngine.AIModule/UnityEngine.Experimental.AI/NavMeshQuery.cs- private static bool GetPortalPoints(IntPtr navMeshQuery, PolygonId polygon, PolygonId neighbourPolygon, out Vector3 left, out Vector3 right) UnityEngine.AIModule/UnityEngine.Experimental.AI/NavMeshQuery.cs- private static Matrix4x4 PolygonLocalToWorldMatrix(IntPtr navMeshQuery, PolygonId polygon) UnityEngine.AIModule/UnityEngine.Experimental.AI/NavMeshQuery.cs- private static Matrix4x4 PolygonWorldToLocalMatrix(IntPtr navMeshQuery, PolygonId polygon) UnityEngine.AIModule/UnityEngine.Experimental.AI/NavMeshQuery.cs- private static NavMeshPolyTypes GetPolygonType(IntPtr navMeshQuery, PolygonId polygon) UnityEngine.UnityAnalyticsModule/UnityEngine.Analytics/UnityAnalyticsHandler.cs- internal static extern void Internal_Destroy(IntPtr ptr); UnityEngine.UnityAnalyticsModule/UnityEngine.Analytics/CustomEventData.cs- internal static extern void Internal_Destroy(IntPtr ptr); UnityEngine.AnimationModule/UnityEngine.Animations/AnimatorControllerPlayable.cs- private static extern int StringToHash(string name); UnityEngine.UnityConnectModule/UnityEngine/RemoteConfigSettings.cs- internal static extern void Internal_Destroy(IntPtr ptr); UnityEngine.GameCenterModule/UnityEngine.SocialPlatforms.GameCenter/GcLeaderboard.cs- private static extern void GcLeaderboard_Dispose(IntPtr leaderboard); UnityEngine.IMGUIModule/UnityEngine/ObjectGUIState.cs- private static extern void Internal_Destroy(IntPtr ptr); UnityEngine.UIElementsModule/UnityEngine.CSSLayout/Native.cs- private static extern void CSSNodeFreeInternal(IntPtr cssNode); UnityEngine.UnityWebRequestModule/UnityEngine.Networking/DownloadHandler.cs- private extern void Release(); UnityEngine.UnityWebRequestModule/UnityEngine.Networking/CertificateHandler.cs- private extern void Release(); UnityEngine.UnityWebRequestModule/UnityEngine.Networking/UploadHandler.cs- private extern void Release(); UnityEngine.UnityWebRequestModule/UnityEngine.Networking/UnityWebRequest.cs- private static extern string GetWebErrorString(UnityWebRequest.UnityWebRequestError err); UnityEngine.UnityWebRequestModule/UnityEngine.Networking/UnityWebRequest.cs- private extern void Release(); UnityEngine.UnityWebRequestModule/UnityEngine.Networking/UnityWebRequest.cs- public extern void Abort(); UnityEngine.ARModule/UnityEngine.XR.Tango/MeshReconstructionServer.cs- internal static extern void DestroyThreaded(IntPtr server); UnityEngine.AnimationModule/UnityEngine/Animator.cs- public static extern int StringToHash(string name); UnityEngine.AudioModule/UnityEngine.Experimental.Audio/AudioSampleProvider.cs- private static extern void InternalSetScriptingPtr(uint providerId, AudioSampleProvider provider); UnityEngine.AudioModule/UnityEngine.Experimental.Audio/AudioSampleProvider.cs- private static extern bool InternalIsValid(uint providerId); UnityEngine.AudioModule/UnityEngine.Experimental.Audio/AudioSampleProvider.cs- private static extern uint InternalGetMaxSampleFrameCount(uint providerId); UnityEngine.AudioModule/UnityEngine.Experimental.Audio/AudioSampleProvider.cs- private static extern uint InternalGetAvailableSampleFrameCount(uint providerId); UnityEngine.AudioModule/UnityEngine.Experimental.Audio/AudioSampleProvider.cs- private static extern uint InternalGetFreeSampleFrameCount(uint providerId); UnityEngine.AudioModule/UnityEngine.Experimental.Audio/AudioSampleProvider.cs- private static extern uint InternalGetFreeSampleFrameCountLowThreshold(uint providerId); UnityEngine.AudioModule/UnityEngine.Experimental.Audio/AudioSampleProvider.cs- private static extern void InternalSetFreeSampleFrameCountLowThreshold(uint providerId, uint sampleFrameCount); UnityEngine.AudioModule/UnityEngine.Experimental.Audio/AudioSampleProvider.cs- private static extern bool InternalGetEnableSampleFramesAvailableEvents(uint providerId); UnityEngine.AudioModule/UnityEngine.Experimental.Audio/AudioSampleProvider.cs- private static extern void InternalSetEnableSampleFramesAvailableEvents(uint providerId, bool enable); UnityEngine.AudioModule/UnityEngine.Experimental.Audio/AudioSampleProvider.cs- private static extern bool InternalGetEnableSilencePadding(uint id); UnityEngine.AudioModule/UnityEngine.Experimental.Audio/AudioSampleProvider.cs- private static extern void InternalSetEnableSilencePadding(uint id, bool enabled); UnityEngine.AudioModule/UnityEngine.Experimental.Audio/AudioSampleProvider.cs- private static extern IntPtr InternalGetConsumeSampleFramesNativeFunctionPtr();
Note that some of these are private, so they can only be used indirectly via a public API. For example, call Debug.Log
to indirectly invoke the private UnityLogWriter.WriteStringToUnityLogImpl
.
There are also quite a few Unity API functions implemented in pure C#. For example, Vector3.Lerp and Color.Lerp don’t make any calls into the engine’s native C++ code. Any of these functions are usable from jobs. It’s also OK to call these functions if they only call into thread-safe native code functions, as described and listed above.
As new versions of Unity are released, more and more of the API will likely become available to jobs. For example, the Unity Roadmap says that the Playable API will be accessible by jobs in 2018.2.
Aside from the Unity API, any other APIs that are thread-safe can be used. It may be hard to find any that are restricted to just the usable types allowed in the job system, but there may be some available. There’s also the opportunity to write new APIs for the job system.
Define a Job
The first step is to define a job. The job should do a small amount of well-defined work, similar to a function. First, define a struct:
public struct ApplyVelocityJob { }
Next, decide what kind of job it is. There are currently three kinds of jobs to choose from:
IJob
: a simple job that executes onceIJobParallelFor
: a job that’s executed for a range of valuesIJobParallelForTransform
: a job that accesses transforms
Let’s start with the simple IJob
and implement it:
public struct ApplyVelocityJob : IJob { public void Execute() { } }
Now we need to define the job’s inputs and outputs. To do so, we add fields to the struct rather than parameters to its Execute
method. This job will modify the position of an object by its corresponding velocity given a span of elapsed time. So let’s add some fields:
public struct ApplyVelocityJob : IJob { public float ElapsedTime; public NativeArray<Vector3> Positions; public NativeArray<Vector3> Velocities; public void Execute() { } }
To allow for greater optimization and error checking, we can optionally add [ReadOnly]
and [WriteOnly]
attributes to these “parameter” fields:
public struct ApplyVelocityJob : IJob { [ReadOnly] public float ElapsedTime; public NativeArray<Vector3> Positions; [ReadOnly] public NativeArray<Vector3> Velocities; public void Execute() { } }
The last step is to actually implement the job’s logic by filling out the Execute
method:
public struct ApplyVelocityJob : IJob { [ReadOnly] public float ElapsedTime; public NativeArray<Vector3> Positions; [ReadOnly] public NativeArray<Vector3> Velocities; public void Execute() { for (int i = 0; i < Positions.Length; ++i) { Positions[i] += Velocities[i] * ElapsedTime; } } }
That’s all there is to it! However, we could easily parallelize this job by making it implement IJobParallelFor
. Instead of running Execute
one time for the entire array, the job system will run Execute
once for each element of the array. Each time it calls Execute
it may use a different CPU core, meaning that the job can make use of all the cores in the system just with a few simple tweaks! Let’s see how to do that:
public struct ApplyVelocityParallelJob : IJobParallelFor { [ReadOnly] public float ElapsedTime; public NativeArray<Vector3> Positions; [ReadOnly] public NativeArray<Vector3> Velocities; public void Execute(int index) { Positions[index] += Velocities[index] * ElapsedTime; } }
Since the for
loop has essentially moved to the job system, it’s arguably even easier to write the parallel version since we don’t need to write the loop anymore.
Finally, if we wanted to modify a game object’s transform in the job then we’d need to implement IJobParallelForTransform
. Let’s modify this job so it doesn’t just use a Vector3
in a NativeArray
but actually change’s the game object’s position:
public struct ApplyVelocityTransformJob : IJobParallelForTransform { [ReadOnly] public float ElapsedTime; [ReadOnly] public NativeArray<Vector3> Velocities; public void Execute(int index, TransformAccess transform) { transform.localPosition += Velocities[index] * ElapsedTime; } }
Create the Job
Creating the job is as easy as creating any other struct. Just call new
and fill in all the fields. The initializer syntax can be handy for this:
ApplyVelocityJob job = new ApplyVelocityJob { ElapsedTime = Time.time, Positions = positions, Velocities = velocities }; ApplyVelocityParallelJob parallelJob = new ApplyVelocityParallelJob { ElapsedTime = Time.time, Positions = positions, Velocities = velocities }; ApplyVelocityTransformJob transformJob = new ApplyVelocityTransformJob { ElapsedTime = Time.time, Velocities = velocities };
Start the Job
Jobs can be started in a few ways. Let’s start simple with the the IJob
:
JobHandle jobHandle = job.Schedule();
Scheduling the job tells the job system that it’s ready to begin. We get a JobHandle
back which we can use to refer to the job in the future. For example, if we want to start a job that has a dependency on another job finishing, we can pass that into Schedule
. For example, to run the same job twice we can have the second job run after the first is complete:
// Schedule with no dependencies JobHandle jobHandle1 = job.Schedule(); // Schedule with dependencies JobHandle jobHandle2 = job.Schedule(jobHandle1);
Now let’s try scheduling an IJobParallelFor
. To do this, we need to provide a couple of extra parameters. The first is the number of iterations the loop should run for. The second is the number of iterations to run in one “batch” on a single core. Rather than a single correct value, this can be tuned larger for small jobs and smaller for large jobs. Here’s how the Schedule
call works:
// Schedule with no dependencies JobHandle jobHandle1 = parallelJob.Schedule(positions.Length, 32); // Schedule with dependencies JobHandle jobHandle2 = parallelJob.Schedule(positions.Length, 32, jobHandle1);
Last up is IJobParallelForTransform
, which takes a TransformAccessArray
parameter. This parameter wraps a managed array of Transform
objects and needs to be manually freed via a Dispose
call, just like with NativeArray
. Here’s how to create one and schedule the job:
// Get or create a managed array of Transform objects Transform[] transforms = new [] { transform }; // Wrap the managed array in a TransformAccessArray TransformAccessArray accessArray = new TransformAccessArray(transforms); // Schedule with no dependencies JobHandle jobHandle1 = transformJob.Schedule(accessArray); // Schedule with dependencies JobHandle jobHandle2 = transformJob.Schedule(accessArray, jobHandle1);
There’s one last step after all jobs are scheduled or there’s a significant amount of work to be done on the main thread before scheduling the next job. We need to tell the job system that we’re ready for it to start executing the scheduled jobs:
JobHandle.ScheduleBatchedJobs();
Jobs are normally scheduled with Schedule
and JobHandle.ScheduleBatchedJobs
, but IJob
and IJobParallelFor
can also be run synchronously so they complete immediately before proceeding. This is not an option for IJobParallelForTransform
though. When doing this, we call Run
with different parameters and there are no dependencies involved. Here’s how to run jobs this way:
job.Run(); parallelJob.Run(positions.Length);
Complex Dependencies
Notice that all of the Schedule
methods take only one JobHandle
parameter to indicate dependencies. This is fine when every job only depends directly on one other job and indirectly on a “linked list” of jobs:
// a -> b -> c JobHandle a = job.Schedule(); JobHandle b = job.Schedule(a); JobHandle c = job.Schedule(b);
Sometimes we need a job to depend on the outcome of two or more jobs. JobHandle CombineDependencies
exists to handle just this case by combining multiple handles into a single handle. There are three overloads depending on the number of handles to combine:
// Combine two handles JobHandle ab = JobHandle.CombineDependencies(a, b); // Combine three handles JobHandle abc = JobHandle.CombineDependencies(a, b, c); // Combine an arbitrary number of handles NativeArray<JobHandle> handles = new NativeArray<JobHandle>(4, Allocator.Persistent); handles[0] = a; handles[1] = b; handles[2] = c; handles[3] = d; JobHandle abcd = JobHandle.CombineDependencies(handles);
With this available to us, we can now express more complicated dependency “trees” rather than just “linked lists”:
// a & b -> c -> d & e // -> f JobHandle a = job.Schedule(); JobHandle b = job.Schedule(); JobHandle ab = JobHandle.CombineDependencies(a, b); JobHandle c = job.Schedule(ab); JobHandle f = job.Schedule(ab); JobHandle d = job.Schedule(c); JobHandle e = job.Schedule(c);
Finishing the Job
Once scheduled, the job system will run jobs to completion automatically. We can, however, ask for the status of those jobs. For starters, there’s a property to check if a job has completed:
if (jobHandle1.IsCompleted) { Debug.Log("Job done"); }
There’s also a function to check whether a job depends on another job. This only returns true
when there is a dependency at the time that Schedule
was called and the dependency job hasn’t finished running, which would make it no longer a dependency.
if (JobHandle.CheckFenceIsDependencyOrDidSyncFence(jobHandle, parallelJobHandle)) { Debug.Log("Job still waiting on Parallel Job"); }
Lastly, if we come to a point in the main thread where we must have the results of a job then we can block execution until it finishes. One way to do this is to call Complete
:
jobHandle.Complete();
If there were multiple “leaves” of the dependency “tree” of jobs, CompleteAll
might be more appropriate as it allows us to wait for multiple jobs to complete. As with CombineDependencies
, there are three overloads:
// Complete two jobs JobHandle.CompleteAll(a, b); // Complete three jobs JobHandle.CompleteAll(a, b, c); // Complete an arbitrary number of jobs NativeArray<JobHandle> handles = new NativeArray<JobHandle>(4, Allocator.Temp); handles[0] = a; handles[1] = b; handles[2] = c; handles[3] = d; JobHandle.CompleteAll(handles);
Where Everything Goes
Using the job system is a new way of writing code in Unity and it’s often not immediately obvious where to put each of these parts. While there’s no one right answer, a typical pattern looks like this:
public class TestScript : MonoBehaviour { // Data that jobs use is stored in fields so it can be disposed later // This includes access to transforms private NativeArray<Vector3> positions; private NativeArray<Vector3> velocities; private TransformAccessArray transformAccessArray; // Job handles are stored in fields to check status and complete them later private JobHandle jobHandle; private JobHandle parallelJobHandle; private JobHandle transformJobHandle; // Alternatives: Awake() and Start() void OnEnable() { // Create long-term data for jobs to process // This includes access to transforms positions = new NativeArray<Vector3>(100, Allocator.Persistent); velocities = new NativeArray<Vector3>(100, Allocator.Persistent); for (int i = 0; i < velocities.Length; ++i) { velocities[i] = Vector3.one; } Transform[] transforms = { transform }; transformAccessArray = new TransformAccessArray(transforms); } // Alternatives: OnDestroy(), OnApplicationQuit() void OnDisable() { // Dispose long-term data for jobs // This includes access to transforms positions.Dispose(); velocities.Dispose(); transformAccessArray.Dispose(); } // Alternatives: FixedUpdate(), but beware multiple calls per frame void Update() { // Create jobs and pass parameters via fields ApplyVelocityJob job = new ApplyVelocityJob { ElapsedTime = Time.time, Positions = positions, Velocities = velocities }; ApplyVelocityParallelJob parallelJob = new ApplyVelocityParallelJob { ElapsedTime = Time.time, Positions = positions, Velocities = velocities }; ApplyVelocityTransformJob transformJob = new ApplyVelocityTransformJob { ElapsedTime = Time.time, Velocities = velocities }; // Schedule jobs including dependencies // Store handles in fields jobHandle = job.Schedule(); parallelJobHandle = parallelJob.Schedule( positions.Length, 32, jobHandle); transformJobHandle = transformJob.Schedule( transformAccessArray, parallelJobHandle); // Start the jobs JobHandle.ScheduleBatchedJobs(); } // Alternatives: this or next frame's Update() or FixedUpdate() private void LateUpdate() { // Wait for the leaves of the job dependency tree to finish transformJobHandle.Complete(); } }
As noted in the comments, there’s some room to move these parts around in the code. The important part is that the main thread is doing all the job creation, scheduling, and completion, including creating and disposing the job parameters. There are game-specific choices to be made here, especially regarding when to wait for the jobs to finish. That may happen later on in the same frame the jobs started or even on a future frame for long-term number crunching.
Conclusion
The job system is remarkably easy to use given how powerful it is. While the Unity API integration has a long way to go, some of the most important functionality (e.g. moving characters around) is already present. For some examples of how it can be used as of 2018.1, check out this GitHub project. If you’ve got any questions, feel free to leave a comment!
#1 by Jayckob on July 24th, 2018 ·
I’m interested to know about how the job system will perform when replacing it with my current implementation, can you speak more to the performance of the job system as compared with an already implemented worker thread system? (My current setup is adding a bunch of values to a queue, the thread runs a method that loops and pops the queue, performs some calculations, then adds the value to another queue).
I’m implementing it as an option to see if it helps and might report back with some figures.
thanks
#2 by jackson on July 24th, 2018 ·
It’s hard for me to compare the performance of Unity’s job system with your own because I don’t have any knowledge of yours except for the little bit you mention in this comment. I’d need a much deeper look to evaluate it properly.
That said, Unity’s job system has a fundamental advantage over C# threads that we can create. Unity maintains its own thread pool with one thread per core. When we create threads, we introduce contention for CPU cores and therefore costly task switching at the OS level. This can lead to frame spikes and generally lower performance.
Feel free to report any figures you come up with for your own system here. I’m curious to see how a script-based system fares against Unity’s job system, especially when fighting for CPU cores under heavy multi-threaded load.
#3 by wang.TS on September 17th, 2020 ·
So do you have any thoughts on handle situations when the TransformAccessArray need to be changed frenquently. like you have gameobjects added and removed every frame
#4 by jackson on September 20th, 2020 ·
In that case, it’d be better to use the other
TransformAccessArray
constructor which sets its capacity and desired job count. Then you can treat it similarly to a dynamic array (e.g.List<T>
) by callingAdd
andRemoveAtSwapBack
to add and removeTransform
objects from it.