Unity Job System笔记

用于代码多线程执行,配合专用于Job System的Burst compiler 性能更好;

unity通过其原生job system,在多个工作线程(worker threads)执行代码;

job system会自动根据cpu的核数来决定线程数量,派发任务时无须考虑cpu核数;

Work stealing:闲的工作线程会窃取忙碌线程的任务来执行;

Safety system:

job里的数据是主线程数据的拷贝,而不是直接传递引用,这样隔离数据避免冲突;

Job types:

IJob:执行单个任务;

IJobParallelFor:并行执行Job里的代码;

IJobParallelForTransform:用于并行处理Transform;

IJobFor:

线程安全类型:

Burst不支持托管对象;为了jobs执行性能,jobs里的数据类型需要非托管的 blittable 类型,或unity的NativeContainer;

NativeContainer是原生内存线程安全的c#包装,同时也允许job和主线程共享数据;

unity内置NativeContainer对象:

NativeArray;

NativeSlice:NativeArray内的一段内存;

Collections package包含额外的NativeContainer;

读写权限:

job里的NativeContainer默认有读写权限,但会影响性能;

job system不允许同时有两个job对NativeContainer有写的权限;

因此需要用[ReadOnly]属性声明:

cs 复制代码
[ReadOnly]
public NativeArray<int> input;

Memory allocators:

创建NativeContainer对象时,需要指定内存分配类型;

Allocator.Temp:

最快的,寿命在一帧内, 不能用于jobs里的成员;

Allocator.TempJob:

寿命在四帧内,必须在四帧内Dispose,否则告警,常用于jobs;

cs 复制代码
NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);

Allocator.Persistent:

性能最慢,可存在任意时长;

NativeContainer safety system:

NativeContainer对象内置safety system,记录哪些job对其读写;

在schedule新的job时,如果有两个独立的job同时具体写的权限,则安全系统抛出异常;

同样适用于主线程,不能在job写完前读取,或在job使用时写入;

NativeContainers 没有实现ref return,不能直接更改NativeContainer的内容:

cs 复制代码
MyStruct temp = myNativeArray[i];
temp.memberVariable = 0;
myNativeArray[i] = temp;

实现一个自定义的 native container:

类添加 NativeContainer 属性;

与safety system集成;

Usage tracking:让unity记录已分配的jobs对NativeContainer的使用情况;

Leak tracking:内存泄露检查;

实现usage tracking:

包含一个类型为结构体 AtomicSafetyHandle ,名 m_Safety 的字段;

AtomicSafetyHandle持有一个引用,指向safety system中为该 native container存储的信息;

AtomicSafetyHandle还存储一些标记,用于指示当前环境可以执行的操作;

job包含一个NativeContainer时,job system自动配置相关标志;

两个job包含同一个NativeContainer,由于NativeContainer和AtomicSafetyHandle为结构体,两个job有不同的标志;

实现 leak tracking:

unity原生代码已经实现的内存泄露跟踪;

使用UnsafeUtility.MallocTracked来分配内存,使用UnsafeUtility.FreeTracked来Dispose;

为了识别NativeContainer创建,可设置NativeLeakDetection.Mode,或者编辑器

Preferences > Jobs > Leak Detection Level;

native containers不支持嵌套native containers;

AtomicSafetyHandle.SetNestedContainer;

AtomicSafetyHandle支持Safety IDs and error messages

ativeContainer structures复制原理:

NativeContainer为结构体,复制指向存储数据的指针;

AtomicSafetyHandle指向相同的safety data,但有不同的标记;

Version numbers:

NativeContainer释放后,对应存储数据的内存被释放,指针引用无效,其他复制的NativeContainer结构体应识别无效;

AtomicSafetyHandle 指向的central record不会被释放,而是被其他NativeContainer复用;

每个record包含一个version number,AtomicSafetyHandle中也包含一个,当释放NativeContainer时,unity增加central record中的version number,然后可被其他复用;

AtomicSafetyHandle通过比较自身和指向的record中的version number来判断是否有效,如CheckReadAndThrow和CheckWriteAndThrow;

Static views of dynamic native containers:

动态原生容器长度可变,如Collections package中的NativeList<T>;

通过叫为view的接口来直接访问数据,view不会复制数据或获取数据的所有权;

如NativeList<T>.AsArray;

在动态容器长度变更时,会重新分配内存,导致view持有的指针失效;

由于引入Secondary version numbers:

safety system在AtomicSafetyHandle中包含第二个version number,与第一个version numer相独立;

创建view前,首先使用AtomicSafetyHandle.CheckGetSecondaryDataPointerAndThrow复制指针安全;

然后复制当前容器的AtomicSafetyHandle,使用UseSecondaryVersion标记新复制的Handle;

变更长度时,使用CheckWriteAndBumpSecondaryVersion替换CheckWriteAndThrow;

AtomicSafetyHandle.SetBumpSecondaryVersionOnScheduleWrite在具有写权限的job派发时自动使view失效;

cs 复制代码
    public NativeArray<T> AsArray()
    {
#if ENABLE_UNITY_COLLECTIONS_CHECKS
        // Check that it's safe for you to use the buffer pointer to construct a view right now.
        AtomicSafetyHandle.CheckGetSecondaryDataPointerAndThrow(m_Safety);

        // Make a copy of the AtomicSafetyHandle, and mark the copy to use the secondary version instead of the primary
        AtomicSafetyHandle handleForArray = m_Safety;
        AtomicSafetyHandle.UseSecondaryVersion(ref handleForArray);
#endif

        // Create a new NativeArray which aliases the buffer, using the current size. This doesn't allocate or copy
        // any data, it just sets up a NativeArray<T> which points at the m_Buffer.
        var array = NativeArrayUnsafeUtility.ConvertExistingDataToNativeArray<T>(m_Buffer, m_Length, Allocator.None);

#if ENABLE_UNITY_COLLECTIONS_CHECKS
        // Set the AtomicSafetyHandle on the newly created NativeArray to be the one that you copied from your handle
        // and made to use the secondary version.
        NativeArrayUnsafeUtility.SetAtomicSafetyHandle(ref array, handleForArray);
#endif

        return array;
    }

特殊的handles:

GetTempMemoryHandle(IsTempMemoryHandle.);

GetTempUnsafePtrSliceHandle;

cs 复制代码
using System;
using System.Runtime.InteropServices;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Collections;

// Marks the struct as a NativeContainer. This tells the job system that it contains an AtomicSafetyHandle.
[NativeContainer]
public unsafe struct NativeAppendOnlyList<T> : IDisposable where T : unmanaged
{
    // Raw pointers aren't usually allowed inside structures that are passed to jobs, but because it's protected
    // with the safety system, you can disable that restriction for it
    [NativeDisableUnsafePtrRestriction]
    internal void* m_Buffer;
    internal int m_Length;
    internal Allocator m_AllocatorLabel;

    // You should only declare and use safety system members with the ENABLE_UNITY_COLLECTIONS_CHECKS define.
    // In final builds of projects, the safety system is disabled for performance reasons, so these APIs aren't
    // available in those builds.
#if ENABLE_UNITY_COLLECTIONS_CHECKS
    
    // The AtomicSafetyHandle field must be named exactly 'm_Safety'.
    internal AtomicSafetyHandle m_Safety;
    
    // Statically register this type with the safety system, using a name derived from the type itself
    internal static readonly int s_staticSafetyId = AtomicSafetyHandle.NewStaticSafetyId<NativeAppendOnlyList<T>>();
#endif

    public NativeAppendOnlyList(Allocator allocator, params T[] initialItems)
    {
        m_Length = initialItems.Length;
        m_AllocatorLabel = allocator;

        // Calculate the size of the initial buffer in bytes, and allocate it
        int totalSize = UnsafeUtility.SizeOf<T>() * m_Length;
        m_Buffer = UnsafeUtility.MallocTracked(totalSize, UnsafeUtility.AlignOf<T>(), m_AllocatorLabel, 1);

        // Copy the data from the array into the buffer
        var handle = GCHandle.Alloc(initialItems, GCHandleType.Pinned);
        try
        {
            UnsafeUtility.MemCpy(m_Buffer, handle.AddrOfPinnedObject().ToPointer(), totalSize);
        }
        finally
        {
            handle.Free();
        }

#if ENABLE_UNITY_COLLECTIONS_CHECKS
        // Create the AtomicSafetyHandle and DisposeSentinel
        m_Safety = AtomicSafetyHandle.Create();

        // Set the safety ID on the AtomicSafetyHandle so that error messages describe this container type properly.
        AtomicSafetyHandle.SetStaticSafetyId(ref m_Safety, s_staticSafetyId);
        
        // Automatically bump the secondary version any time this container is scheduled for writing in a job
        AtomicSafetyHandle.SetBumpSecondaryVersionOnScheduleWrite(m_Safety, true);

        // Check if this is a nested container, and if so, set the nested container flag
        if (UnsafeUtility.IsNativeContainerType<T>()) 
            AtomicSafetyHandle.SetNestedContainer(m_Safety, true);
#endif
    }

    public int Length
    {
        get
        {
#if ENABLE_UNITY_COLLECTIONS_CHECKS
            // Check that you are allowed to read information about the container 
            // This throws InvalidOperationException if you aren't allowed to read from the native container,
            // or if the native container has been disposed
            AtomicSafetyHandle.CheckReadAndThrow(m_Safety);
#endif
            return m_Length;
        }
    }

    public T this[int index]
    {
        get
        {
#if ENABLE_UNITY_COLLECTIONS_CHECKS
            // Check that you can read from the native container right now.
            AtomicSafetyHandle.CheckReadAndThrow(m_Safety);
#endif

            // Read from the buffer and return the value
            return UnsafeUtility.ReadArrayElement<T>(m_Buffer, index);
        }

        set
        {
#if ENABLE_UNITY_COLLECTIONS_CHECKS
            // Check that you can write to the native container right now.
            AtomicSafetyHandle.CheckWriteAndThrow(m_Safety);
#endif
            // Write the value into the buffer
            UnsafeUtility.WriteArrayElement(m_Buffer, index, value);
        }
    }

    public void Add(T value)
    {
#if ENABLE_UNITY_COLLECTIONS_CHECKS
        // Check that you can modify (write to) the native container right now, and if so, bump the secondary version so that
        // any views are invalidated, because you are going to change the size and pointer to the buffer
        AtomicSafetyHandle.CheckWriteAndBumpSecondaryVersion(m_Safety);
#endif

        // Replace the current buffer with a new one that has space for an extra element
        int newTotalSize = (m_Length + 1) * UnsafeUtility.SizeOf<T>();
        void* newBuffer = UnsafeUtility.MallocTracked(newTotalSize, UnsafeUtility.AlignOf<T>(), m_AllocatorLabel, 1);
        UnsafeUtility.MemCpy(newBuffer, m_Buffer, m_Length * UnsafeUtility.SizeOf<T>());
        UnsafeUtility.FreeTracked(m_Buffer, m_AllocatorLabel);
        m_Buffer = newBuffer;
        
        // Put the new element at the end of the buffer and increase the length
        UnsafeUtility.WriteArrayElement(m_Buffer, m_Length++, value);
    }

    public NativeArray<T> AsArray()
    {
#if ENABLE_UNITY_COLLECTIONS_CHECKS
        // Check that it's safe for you to use the buffer pointer to construct a view right now.
        AtomicSafetyHandle.CheckGetSecondaryDataPointerAndThrow(m_Safety);
        
        // Make a copy of the AtomicSafetyHandle, and mark the copy to use the secondary version instead of the primary
        AtomicSafetyHandle handleForArray = m_Safety;
        AtomicSafetyHandle.UseSecondaryVersion(ref handleForArray);
#endif

        // Create a new NativeArray which aliases the buffer, using the current size. This doesn't allocate or copy
        // any data, it just sets up a NativeArray<T> which points at the m_Buffer.
        var array = NativeArrayUnsafeUtility.ConvertExistingDataToNativeArray<T>(m_Buffer, m_Length, Allocator.None);
        
#if ENABLE_UNITY_COLLECTIONS_CHECKS
        // Set the AtomicSafetyHandle on the newly created NativeArray to be the one that you copied from your handle
        // and made to use the secondary version.
        NativeArrayUnsafeUtility.SetAtomicSafetyHandle(ref array, handleForArray);
#endif
        
        return array;
    }

    public void Dispose()
    {
#if ENABLE_UNITY_COLLECTIONS_CHECKS
        AtomicSafetyHandle.CheckDeallocateAndThrow(m_Safety);
        AtomicSafetyHandle.Release(m_Safety);
#endif

        // Free the buffer
        UnsafeUtility.FreeTracked(m_Buffer, m_AllocatorLabel);
        m_Buffer = null;
        m_Length = 0;
    }
}

创建和运行一个 job:

创建job:实现IJob接口;

实现Execute方法,worker thread执行job时调用;

Schedule job:在job上调用Schedule方法;

只能在主线程调用;

还有个Run方法,在主线程同步执行;

等等job完成:在JobHandle上调用Complete方法,然后访问结果数据;

尽可能迟调用,尽量在job执行完成调用,否则会阻塞主线程;

从job中直接访问非readonly或可变静态数据没有保护措施;

主线程在空闲时也会执行job,因此要注意job需在一帧内完成;

cs 复制代码
public class TempTest : MonoBehaviour
{
    // Create a native array of a single float to store the result. Using a 
    // NativeArray is the only way you can get the results of the job, whether
    // you're getting one value or an array of values.
    NativeArray<float> result;
    // Create a JobHandle for the job
    JobHandle handle;

    // Set up the job
    public struct MyJob : IJob
    {
        public float a;
        public float b;
        public NativeArray<float> result;

        public void Execute()
        {
            result[0] = a + b;
        }
    }

    // Update is called once per frame
    void Update()
    {
        // Set up the job data
        result = new NativeArray<float>(1, Allocator.TempJob);

        MyJob jobData = new MyJob
        {
            a = 10,
            b = 10,
            result = result
        };

        // Schedule the job
        handle = jobData.Schedule();
    }

    private void LateUpdate()
    {
        // Sometime later in the frame, wait for the job to complete before accessing the results.
        handle.Complete();

        // All copies of the NativeArray point to the same memory, you can access the result in "your" copy of the NativeArray
        float aPlusB = result[0];

        // Free the memory allocated by the result array
        result.Dispose();
    }
}

对于IJobParallelFor,注意增加 batch size来减少执行的线程数;

cs 复制代码
MyParallelJob jobData = new MyParallelJob();
jobData.result = result;
// Use half the available worker threads, clamped to a minimum of 1 worker thread
int numBatches = Math.Max(1, JobsUtility.JobWorkerCount / 2);
int totalItems = result.Length;
int batchSize = totalItems / numBatches;
// Schedule the job with one Execute per index in the results array and batchSize items per processing batch
JobHandle handle = jobData.Schedule(totalItems, batchSize);

Job 依赖:

一个job可依赖另一个job的执行结果,job system等依赖的job完成时才执行当前的job;

cs 复制代码
JobHandle firstJobHandle = firstJob.Schedule();
secondJob.Schedule(firstJobHandle);

多个依赖时:

cs 复制代码
NativeArray<JobHandle> handles = new NativeArray<JobHandle>(numJobs, Allocator.TempJob);

// Populate `handles` with `JobHandles` from multiple scheduled jobs...

JobHandle jh = JobHandle.CombineDependencies(handles);

Parallel jobs:

同时对多个对象执行相同的操作,多核运行,每个核上一个job,处理若干batch;

cs 复制代码
public struct MyParallelJob : IJobParallelFor
{
    public NativeArray<float> result;

    public void Execute(int index)
    {
        result[index] = index;
    }
}

Schedule时需要指定数据长度和batch大小:

未执行的Batch也会被空闲的job窃取,每次窃取剩余的一半;

ParallelForTransform:

专门操作Transform的IJobParallelFor;

相关推荐
winlife_3 小时前
Funplay Unity MCP 与 Unity AI Assistant 详细对比:开源 MCP 工具集 vs 官方全栈 AI 产品
人工智能·unity·开源·ai编程·claude·mcp
御水流红叶3 小时前
Android-Unity游戏逆向思路
android·游戏·unity
ellis19705 小时前
Unity图集Atlas
unity
想不明白的过度思考者5 小时前
Unity全局事件中心与新版输入架构实现练习——上帝模式与英雄模式的输入系统映射切换
java·unity·架构
GLDbalala19 小时前
Unity基于自定义管线实现风格化水
unity·游戏引擎
WMX101220 小时前
Unity-登录界面UI制作
ui·unity·游戏引擎
吾日吾身三摆烂1 天前
Unity协程(Coroutine)底层原理全解析
unity·游戏引擎
LF男男1 天前
StarBullect.cs
unity
UWA1 天前
Unity小游戏优化简谱 | 吃透底层逻辑,告别掉帧与流失
unity·性能优化·游戏引擎·小游戏开发