介绍非托管集合和C# 作业系统的核心概念和使用方法。
C#作业系统
Unity Job System 主要用于多线程处理作业任务,缩短运行时间。不同的作业类型,工作线程池内的线程数量不同,线程越多,cpu开销越大,需要根据具体的任务需求进行选择。并行处理时,线程上限由电脑的处理器配置决定,我的是8核,所以并行处理的线程最多为8个。

定义作业类型
需声明一个实现IJob或其他作业接口(如IJobParallelFor, IJobEntity, IJobChunk.等)的结构体(struct)
示例1:FindNearestJob
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
/// <summary>
/// burst 编译器进行编译 的结构体,单线程处理
/// </summary>
[BurstCompile]
public struct FindNearestJob : IJob
{
/// <summary>
/// 用 Unity.Mathematics.float3 替代 Vector3
/// </summary>
[ReadOnly]
public NativeArray<float3> TargetPositions;
[ReadOnly]
public NativeArray<float3> SeekertPositions;
public NativeArray<float3> NearestTargetPositions;
public void Execute()
{
for (int i = 0; i < SeekertPositions.Length; i++)
{
float3 seekerPosition = SeekertPositions[i];
float nearestDistSq = float.MaxValue;
for (int j = 0; j < TargetPositions.Length; j++)
{
float3 targetPosition = TargetPositions[j];
//用 Unity.Mathematics.math.distancesq替代 Vector3.sqrMagnitude.
float distSq = math.distancesq(seekerPosition, targetPosition);
if(distSq< nearestDistSq)
{
nearestDistSq = distSq;
NearestTargetPositions[i] = targetPosition;
}
}
}
}
}
备注:Unity 的 Job System 是为高性能多线程处理设计的,但由于其运行在 Burst 编译器和安全系统下,对托管类型的使用有严格限制。
扩展:可安全使用的托管类型
Job System 主要设计用于非托管代码,但以下托管类型可以在特定情况下使用:
1. 基本值类型(完全支持)
-
int,float,double,bool,char等基本数值类型 -
Unity.Mathematics中的类型(如float3,quaternion,int4等)
2. 结构体(有限支持)
-
简单的
struct类型(仅包含值类型字段) -
必须不包含:
-
引用类型字段
-
指针
-
托管数组
-
字符串
-
类实例
-
3. 特殊容器(通过 NativeContainer)
-
NativeArray<T> -
NativeList<T> -
NativeHashMap<TKey, TValue> -
NativeQueue<T> -
其他 Unity.Collections 命名空间下的容器
4. 受限使用的托管类型
-
string(只读访问,不能修改) -
枚举类型(完全支持)
不可使用的托管类型
-
任何类(
class)类型 -
托管数组(
T[]) -
委托(
delegate) -
接口(
interface) -
动态分配的复杂对象
作业调度
调用扩展方法 Schedule()将作业实例加入作业队列,等待工作线程执行(调用Execute()方法)。
示例2:FindNearestJob实例化及调度处理
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
/// <summary>
/// 查找最近的目标画线
/// </summary>
public class FindNearest : MonoBehaviour
{
NativeArray<float3> TargetPositions;
NativeArray<float3> SeekerPositions;
NativeArray<float3> NearestTargetPositions;
private void Start()
{
Spawner spawner = Object.FindAnyObjectByType<Spawner>();
//永久分配器,因为在程序运行期间一直使用,手动回收
TargetPositions = new NativeArray<float3>(spawner.NumTargets, Allocator.Persistent);
SeekerPositions = new NativeArray<float3>(spawner.NumSeeks, Allocator.Persistent);
NearestTargetPositions = new NativeArray<float3>(spawner.NumSeeks, Allocator.Persistent);
}
// We are responsible for disposing of our allocations
// when we no longer need them.
//手动释放分配器
private void OnDestroy()
{
TargetPositions.Dispose();
SeekerPositions.Dispose();
NearestTargetPositions.Dispose();
}
// Update is called once per frame
void Update()
{
//copy every target transform to a NativeArray
//将目标的Transform信息复制到原生数组
for (int i = 0; i < TargetPositions.Length; i++)
{
// Vector3 is implicitly converted to float3
//Vector3隐式转换为float3
TargetPositions[i]= Spawner.TargetTransforms[i].localPosition;
}
//copy every seeker transform to a NativeArray
//复制seeker位置信息到SeekerPositions
for (int i = 0; i < SeekerPositions.Length; i++)
{
SeekerPositions[i] = Spawner.SeekerTransforms[i].localPosition;
}
// To schedule a job, we first need to create an instance and
// populate its fields.
//实例化 FindNearestJob
FindNearestJob findJob = new FindNearestJob
{
TargetPositions = TargetPositions,
SeekertPositions = SeekerPositions,
NearestTargetPositions = NearestTargetPositions,
};
//调度:将finfjob加入作业队列中
JobHandle findHandle = findJob.Schedule();
//等待作业完成
findHandle.Complete();
for (int i = 0; i < SeekerPositions.Length; i++)
{
// float3 隐式转换 Vector3
Debug.DrawLine(SeekerPositions[i], NearestTargetPositions[i]);
}
}
}
注意:作业仅可从主线程调度,不可从其他作业内部调度。IJob为单线程作业,需等待当前作业执行完成后,才会处理下一个等待中的作业。
作业依赖系统
JobHandle
通过 JobHandle 管理作业间的依赖关系:确保数据访问顺序正确,避免竞态条件的达成
备注: 工作线程只有在作业的所有依赖项都执行完成后,才会取出执行完成的作业。
示例 3:根据二分查找获取最近目标
using System.Collections.Generic;
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
[BurstCompile]
public struct FindNearestByBinarySearchParallelJob : IJobParallelFor
{
[ReadOnly]
public NativeArray<float3> TargetPositions;
[ReadOnly]
public NativeArray<float3> SeekerPositions;
public NativeArray<float3> NearestTargetPositions;
public void Execute(int index)
{
float3 seekerPos = SeekerPositions[index];
//对已排序的TargetPositions 进行二分查找 ,
//不存在返回负值
//存在返回下标
//AxisXComparer 自定义的比较器
int startIdx = TargetPositions.BinarySearch(seekerPos,new AxisXComparer { } );
//取反获得插入位置
//在二分查找(Binary Search)中,取反运算(~)用于处理查找失败时返回的负值,将其转换为正确的插入位置
if (startIdx<0)startIdx=~startIdx;
if (startIdx >= TargetPositions.Length) startIdx = TargetPositions.Length - 1;
//将target的x值最接近seeker的x值的tartget设为最近的目标
float3 nearestTargetPos = TargetPositions[startIdx];
//math.distancesq() 计算两个点之间的平方距离(distance squared),而不是实际距离.
//平方距离公式:(x2-x1)² + (y2-y1)² + (z2-z1)²
float nearestDistSq = math.distancesq(seekerPos, nearestTargetPos);
//向上向下查找
Search(seekerPos,startIdx+1,TargetPositions.Length,+1, ref nearestTargetPos,ref nearestDistSq);
Search(seekerPos, startIdx - 1, -1, -1, ref nearestTargetPos, ref nearestDistSq);
//取得最近的目标
NearestTargetPositions[index] = nearestTargetPos;
}
/// <summary>
/// 向上向下查找,ref 双向传递,不可为空
/// </summary>
/// <param name="seekerPos"></param>
/// <param name="startIdx"></param>
/// <param name="endIdx"></param>
/// <param name="step"></param>
/// <param name="nearestTargetPos"></param>
/// <param name="nearestDistSq"></param>
void Search(float3 seekerPos, int startIdx, int endIdx, int step,
ref float3 nearestTargetPos, ref float nearestDistSq)
{
for (int i = startIdx; i != endIdx; i += step)
{
float3 targetPos = TargetPositions[i];
float xdiff = seekerPos.x - targetPos.x;
// If the square of the x distance is greater than the current nearest, we can stop searching.
//若xdiff平方大于最近的距离
//=>(xt-xs)²> (xn-xs)² + (yn-ys)² + (zn-zs)²
//=> (xt-xs)² + (yt-ys)² + (zt-zs)²>(xn-xs)² + (yn-ys)² + (zn-zs)²
//=>对于已经排序的集合, xdiff*xdiff呈现增长趋势,所以当前条件达成时,停止查找
if ((xdiff * xdiff) > nearestDistSq) break;
float distSq = math.distancesq(targetPos, seekerPos);
if (distSq < nearestDistSq)
{
nearestDistSq = distSq;
nearestTargetPos = targetPos;
}
}
}
}
public struct AxisXComparer : IComparer<float3>
{
public int Compare(float3 x, float3 y)
{
return x.x.CompareTo(y.x);// 按X轴坐标比较
}
}
示例4:针对排序和查找创建依赖关系
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
/// <summary>
/// 对目标数组进行排序,通过二分查找进行算法优化
/// </summary>
public class FindNearestBySort : MonoBehaviour
{
NativeArray<float3> TargetPostions;
NativeArray<float3> SeekerPositions;
NativeArray<float3> NearestTargetPositions;
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
Spawner spawner = Object.FindAnyObjectByType<Spawner>();
///对其进行永久分配
TargetPostions = new NativeArray<float3>(spawner.NumTargets, Allocator.Persistent);
SeekerPositions = new NativeArray<float3>(spawner.NumSeeks, Allocator.Persistent);
NearestTargetPositions = new NativeArray<float3>(spawner.NumSeeks, Allocator.Persistent);
}
// Update is called once per frame
void Update()
{
//刷新位置信息
for (int i = 0; i < TargetPostions.Length; i++)
{
TargetPostions[i] = Spawner.TargetTransforms[i].localPosition;//隐式转换
}
for (int i = 0; i < SeekerPositions.Length; i++)
{
SeekerPositions[i] = Spawner.SeekerTransforms[i].localPosition;//隐式转换
}
//unity.collection 包里封装的SortJob, 根据自定义比较器进行排序
SortJob<float3, AxisXComparer> sortJob = TargetPostions.SortJob(new AxisXComparer { });
JobHandle sortHandle = sortJob.Schedule();
//执行任务,实例化job
FindNearestByBinarySearchParallelJob findNearestParallelJob = new FindNearestByBinarySearchParallelJob
{
TargetPositions = TargetPostions,
SeekerPositions = SeekerPositions,
NearestTargetPositions = NearestTargetPositions
};
///每个线程分配100个任务,等待排序任务完成后,执行
JobHandle jobHandle = findNearestParallelJob.Schedule(SeekerPositions.Length, 100, sortHandle);
jobHandle.Complete();
}
}
JobHandle.CombineDependencies()
使用 JobHandle.CombineDependencies()将多个 JobHandle 合并为一个,表示这些任务全部完成后才会触发后续任务。
示例5:JobHandle.CombineDependencies()
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using UnityEngine;
public class CombineDependenciesExample : MonoBehaviour
{
private void Start()
{
// 创建三个独立的 NativeArray
NativeArray<float> arrayA = new NativeArray<float>(100, Allocator.TempJob);
NativeArray<float> arrayB = new NativeArray<float>(100, Allocator.TempJob);
NativeArray<float> arrayC = new NativeArray<float>(100, Allocator.TempJob);
// 定义三个独立的 Job
var jobA = new InitJob { Data = arrayA, Value = 1f };
var jobB = new InitJob { Data = arrayB, Value = 2f };
var jobC = new InitJob { Data = arrayC, Value = 3f };
// 并行调度三个 Job(无依赖关系)
JobHandle handleA = jobA.Schedule();
JobHandle handleB = jobB.Schedule();
JobHandle handleC = jobC.Schedule();
// 合并三个 Job 的依赖关系
JobHandle combinedHandle = JobHandle.CombineDependencies(handleA, handleB, handleC);
// 定义一个依赖 combinedHandle 的后续 Job
var sumJob = new SumJob
{
ArrayA = arrayA,
ArrayB = arrayB,
ArrayC = arrayC,
Result = new NativeArray<float>(1, Allocator.TempJob)
};
JobHandle finalHandle = sumJob.Schedule(combinedHandle);
// 等待所有任务完成
finalHandle.Complete();
// 打印结果
Debug.Log($"Total Sum: {sumJob.Result[0]}");
// 释放内存
arrayA.Dispose();
arrayB.Dispose();
arrayC.Dispose();
sumJob.Result.Dispose();
}
}
[BurstCompile]
public struct InitJob : IJob
{
public NativeArray<float> Data;
public float Value;
public void Execute()
{
for (int i = 0; i < Data.Length; i++)
{
Data[i] = Value; // 初始化数组为指定值
}
}
}
[BurstCompile]
public struct SumJob : IJob
{
public NativeArray<float> ArrayA;
public NativeArray<float> ArrayB;
public NativeArray<float> ArrayC;
public NativeArray<float> Result;
public void Execute()
{
float sum = 0f;
for (int i = 0; i < ArrayA.Length; i++)
{
sum += ArrayA[i] + ArrayB[i] + ArrayC[i]; // 对三个数组求和
}
Result[0] = sum; // 存储结果
}
}
作业完成
作业执行完成的流程:
1.递归完成该作业的所有依赖项(包括依赖项的依赖项,递归执行)
2.等待该作业执行完成(若该作业尚未执行完成)
3.从作业队列中移除该作业的所有剩余引用
备注:
- 调用已完成作业的JobHandle的Complete()方法不会执行任何东西也不会报错。
- 仅可在主线程完成作业,在作业内部调用Complete()属于非法操作。
- 虽然作业可以在调度后立即调用Complete(),最好延迟到工作真正需要结果的最后一刻调用。通常来说,在作业的调度和完成之间的间隔越长,主线程和工作线程非必要空转(等待状态)将花费时间的可能性就越低。(延迟同步策略)
在作业中的数据存取
通用规则:
- 作业不应该执行I/O操作
- 作业不应该访问托管对象
- 作业仅应访问只读静态字段
调度作业时会创建一个私有副本,该副本仅对正在执行的作业可见。所以对作业结构体字段的任何修改操作仅在作业内部有效。但作业的字段包含指针或者引用类型,则作业对这个外部数据的修改可在作业外部可见。(作业数据隔离机制)
非托管集合(Unmanaged collections)
Unity.Collections package 的非托管集合类型相较于常规的C#托管集合的优势:
- 1. 非托管对象可在Burst编译代码中使用。
- 2.非托管对象可用于作业中(而托管对象在作业中的使用通常是非线程安全的)。
- 3.Native-集合类型内置作业安全检测,强制保障线程安全。
- 4 .非托管对象不受垃圾回收机制管理,因此不会产生GC开销。
注意:需要手动调用Dispose()方法释放不再使用的非托管集合。若未及时释放会导致内存泄露。虽然,Native-集合通过内置的安全检测可捕捉一些(非全部)此类泄露并抛出错误。
分配器(Allocators)
实例化一个非托管集合时,必须指定分配器进行内存分配。不同的分配器以不同的方式组织和追踪内存。最常见的三种分配器是:
Allocator.Persistent:
- 最慢的分配器。
- 长期存在(手动释放)
- 用于生命周期不确定的内存分配。
- 分配的内存必须手动释放。
- 分配的集合能传递到作业中。
TargetPostions = new NativeArray<float3>(spawner.NumTargets,Allocator.Persistent);
SeekerPositions = new NativeArray<float3>(spawner.NumSeeks, Allocator.Persistent);
NearestTargetPositions = new NativeArray<float3>(spawner.NumSeeks,Allocator.Persistent);
//手动释放分配器
private void OnDestroy()
{
TargetPositions.Dispose();
SeekerPositions.Dispose();
NearestTargetPositions.Dispose();
}
Allocator.Temp:
- 最快的分配器。
- 最短生命期(1帧)。
- 不需要手动释放Temp分配器,会在作业结束时自动释放。
- 分配的集合不能传递到作业中。
- 在作业内使用Allocator.Temp是线程安全的。
Allocator.TempJob:
- 速度介于中间层的分配器
- 作业生命期(4帧)
- 分配的内存必须手动释放。若启用释放安全检查,分配的内存若在分配后 4 帧内未释放,系统将抛出异常。
- 分配的集合能传递到作业中。
作业安全检测(Job Safety Checks)
任何两个访问相同数据的作业,需要避免其执行过程出现重叠或者执行顺序不确定的情况。否则当任意作业修改数组时,可能会对另一作业产生干扰:结果取决于一个作业是否在另一作业之前运行、两者执行是否重叠等偶然因素,最终可能导致一个或两个作业产生错误结果。
因此,若两个作业之间存在此类数据冲突,应选择以下一种方式:
- 先完整调度并执行完一个作业,再调度另一个
- 或将一个作业设为另一个的依赖项后再调度
调用Schedule()时,若启用作业安全检查(默认启用),系统在检测到潜在竞态条件时会抛出异常。
一些特殊的情况:
- 当两个作业仅读取同一份数据时,访问是线程安全的。由于没有作业会修改数据,它们彼此不会产生干扰。通过在结构体字段上添加 [ReadOnly] 属性,可声明某个原生数组或集合在作业中仅用于读取。若两个作业共享的所有原生数组/集合均被标记为 [ReadOnly],作业安全检查将判定它们不存在冲突。
- 若需完全禁用作业安全检查机制对某个原生数组/集合的约束,可使用 [NativeDisableContainerSafetyRestriction] 属性标记该字段。但需自行确保不会因此引发竞态条件!
- 当原生容器(Native Collection)被任何已调度的作业使用时,若主线程尝试读取或修改该容器,安全检查系统将抛出异常。作为特殊情况,若该容器在作业中被标记为 [ReadOnly],则允许主线程读取其内容。
并行作业(Parallel Jobs)
通过多线程并行处理数组或列表,可通过实现IJobParallelFor 接口来定义作业。
当作业执行时,Execute() 方法会被调用count次,从0到count-1的整数值传递给index参数。
作业的索引会被按照批次大小分割成多个批次,然后工作线程可分别从队列中取出这些批次。从运行效果来看,不同的批次也许被不同的线程并行处理,但同批次内的所有索引将在单个线程中集中处理。
示例6:定义并行作业
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
/// <summary>
/// 实现IJobParallelFor并行任务处理
/// </summary>
[BurstCompile]
public struct FindNearestParallelJob : IJobParallelFor
{
/// <summary>
/// Execute处理时不可修改,初始化结构体时进行赋值
/// </summary>
[ReadOnly]
public NativeArray<float3> TargetPositions;
[ReadOnly]
public NativeArray<float3> SeekerPositions;
public NativeArray<float3> NearestTargetPositions;
public void Execute(int index)
{
//对数据进行业务处理
float3 seekerPos = SeekerPositions[index];
float nearestDisSq = float.MaxValue;
for (int i = 0; i < TargetPositions.Length; i++)
{
float3 targetPos = TargetPositions[i];
float distSq = math.distancesq(seekerPos, targetPos);//x^2+y^2+Z^2
if(nearestDisSq< distSq)
{
nearestDisSq = distSq;
NearestTargetPositions[index] = targetPos;
}
}
}
}
示例7:并行作业实例化
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
public class FindNearestParallel : MonoBehaviour
{
NativeArray<float3> TargetPostions;
NativeArray<float3> SeekerPositions;
NativeArray<float3> NearestTargetPositions;
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
Spawner spawner = Object.FindAnyObjectByType<Spawner>();
///对其进行永久分配
TargetPostions = new NativeArray<float3>(spawner.NumTargets,Allocator.Persistent);
SeekerPositions = new NativeArray<float3>(spawner.NumSeeks, Allocator.Persistent);
NearestTargetPositions = new NativeArray<float3>(spawner.NumSeeks, Allocator.Persistent);
}
// Update is called once per frame
void Update()
{
//刷新位置信息
for (int i = 0; i < TargetPostions.Length; i++)
{
TargetPostions[i] = Spawner.TargetTransforms[i].localPosition;//隐式转换
}
for (int i = 0; i < SeekerPositions.Length; i++)
{
SeekerPositions[i] = Spawner.SeekerTransforms[i].localPosition;//隐式转换
}
//执行任务
FindNearestParallelJob findNearestParallelJob = new FindNearestParallelJob
{
TargetPositions = TargetPostions,
SeekerPositions=SeekerPositions,
NearestTargetPositions= NearestTargetPositions
};
///每个线程分配100个任务
JobHandle jobHandle = findNearestParallelJob.Schedule(SeekerPositions.Length, 100);
jobHandle.Complete();
}
}
备注:过多的微批次也许会导致作业系统显著的开销。
当处理一个批次时,该批次应当仅访问其指定范围内的数组或列表索引。若访问的索引值超出当前批次的索引参数范围,安全检测机制会在访问数组或列表时抛出异常。
标记了[ReadOnly]属性的数组和列表字段,安全检测机制将不会抛出异常。对于需要在作业中执行写入操作的数组或列表,可以通过为字段添加[NativeDisableParallelForRestriction]属性来屏蔽安全检测机制。但需注意:屏蔽安全检测机制可能导致竞态条件,请谨慎操作!
unity6.0 下载链接:NoUnityCN | Unity国际版下载站 - 让游戏开发更加简单
参考链接:
Get started with Unity's job system - Unity Learn
https://github.com/Unity-Technologies/EntityComponentSystemSamples
unity package manual :
550

被折叠的 条评论
为什么被折叠?



