unity中使用Job
·
unity中使用Job
-
效率最高,unity 专门开辟了一个线程池来执行 Job,用这个执行多线程任务效率最高
- 首先直接用线程池中的线程,省掉线程分配和释放
- 其次会根据cpu数量分配合适的线程数量,避免恶性竞争cpu,产生过多的上下文切换
-
应用场景
- 1个任务,只能用异步
用同步会在主线程执行,不会提升效率 - 1个任务同时依赖其它多个任务,可以用异步或同步
不管用同步或异步都会在其它线程执行 - 1个并行任务,可以用异步或同步,如果任务重,照样会卡,此时只能用异步
用同步会给主线程也分配任务,用异步不会给主线程分配任务
- 1个任务,只能用异步
-
普通作业
// 定义作业,就是定义实现 IJob 的结构体 // 必须是结构体,因为要进行克隆操作 public struct MyJob : IJob { // 成员变量,只能是 blittable 类型,就是常用的可直接跟C通信的值类型 // 和 NativeContainer 类型,比如 NativeArray NativeSlice NativeList NativeQueue 等 // NativeContainer 类型其实就是c内存的c#包装类 // 因为在别的线程实际上是复制了一个结构体过去执行时,因此值类型成员其实是拷贝了一份 // 只有 NativeContainer 成员指向的内存是相同的,因此返回值必须放在 NativeContainer 内 [ReadOnly] // 只读,一般用于输入参数 [DeallocateOnJobCompletion] // 任务完成自动调用 Dispose public NativeArray<float> data; public float a; public float b; [WriteOnly] // 只写,一般用于返回值 public NativeArray<float> result; // 实现 Execute 函数 // MyJob 实例将被复制一份在多线程中执行 // 因此 MyJob 必须是结构体 public void Execute() { Debug.Log($"Execute ThreadId={Thread.CurrentThread.ManagedThreadId}"); result[0] = a + b; } } public class TestJob { // 创建作业 private static MyJob CreateJob() { MyJob jobData = new MyJob(); jobData.a = 10; jobData.b = 10; jobData.result = new NativeArray<float>(1, Allocator.TempJob); // 必须用 Allocator.TempJob 或 Allocator.Persistent return jobData; } private static void ReleaseJob(MyJob job) { // 释放由结果数组分配的内存 job.result.Dispose(); } // 调度作业 private static JobHandle ScheduleJob(MyJob job) { // 调度作业,只是把作业放到队列中,并没有开始执行 // 只能在主线程调用 Schedule // 如果一个作业依赖于另一个作业,请使用 JobHandle 来管理依赖关系,而不是尝试在作业中调度作业 JobHandle handle = job.Schedule(); return handle; } // 测试单个作业 // asyncExecute 是否异步执行 public static IEnumerator TestSingleJob(bool asyncExecute) { MyJob job = CreateJob(); JobHandle handle = ScheduleJob(job); // 执行任务 yield return ExecuteJobHandle(handle, asyncExecute); // NativeArray 的所有副本都指向同一内存,您可以在"您的"NativeArray 副本中访问结果 // 只有调用 Complete 后才能安全的访问 result float aPlusB = job.result[0]; ReleaseJob(job); } // 测试作业可以依赖另一个作业 // asyncExecute 是否异步执行 public static IEnumerator TestDependJob(bool asyncExecute) { MyJob job1 = CreateJob(); JobHandle handle1 = ScheduleJob(job1); MyJob job2 = CreateJob(); JobHandle handle2 = ScheduleJob(job2); MyJob job3 = CreateJob(); // 要等待多个任务,需要先将多个 JobHandle 合并成一个 NativeArray<JobHandle> handles = new NativeArray<JobHandle>(2, Allocator.TempJob); handles[0] = handle1; handles[1] = handle2; JobHandle job12 = JobHandle.CombineDependencies(handles); JobHandle handle3 = job3.Schedule(job12); MyJob job4 = CreateJob(); // 等待单个任务,直接传参数就行 JobHandle handle4 = job4.Schedule(handle3); // 执行任务 yield return ExecuteJobHandle(handle4, asyncExecute); ReleaseJob(job1); ReleaseJob(job2); ReleaseJob(job3); ReleaseJob(job4); handles.Dispose(); } // 执行作业 private static IEnumerator ExecuteJobHandle(JobHandle handle, bool asyncExecute) { if (asyncExecute) { // 异步执行 // 启动作业,所有作业都在非主线程执行 JobHandle.ScheduleBatchedJobs(); // 等待作业完成 yield return new WaitUntil(() => handle.IsCompleted); // 仍然要调用 Complete 才能访问结果 handle.Complete(); // 如果有用 UniTask 的话,已经在 UnityAsyncExtensions 中扩展了 JobHandle // 可以直接使用 await jobHandle; 或 awiat jobHandle.ToUniTask(PlayerLoopTiming.Update); } else { // 同步执行 // 启动作业,并等待作业完成 // 如果只是单个 job 执行,则直接在主线程中执行,所以单个任务不建议这样用,效率没有提升 // 如果 job 有依赖其它 job,则所有 job 都在非主线程执行 // 主线程会等待任务完成后继续执行 handle.Complete(); } } } -
并行作业
// 并行作业,就是有对象数组,你分开在多个线程上分别对某个索引的对象执行操作 // 将两个浮点值相加的作业,必须实现接口 IJobParallelFor public struct MyParallelJob : IJobParallelFor { [ReadOnly] // 只读,一般用于输入参数 public NativeArray<float> a; // 任务完成后自动调用 Dispose(),不能用于返回值 [DeallocateOnJobCompletion] public NativeArray<float> b; public float scale; [WriteOnly] // 只写,一般用于返回值 public NativeArray<float> result; // 会有一个索引参数 // MyParallelJob 实例将被复制多份,在多个线程中执行 // 只有 NativeContainer 指向同一份内存,因此返回值只能放这里 public void Execute(int i) { Debug.Log($"Execute index={i} ThreadId={Thread.CurrentThread.ManagedThreadId}"); result[i] = a[i] + b[i] * scale; } } public static class TestParrallelJob { static int num = 8; private static MyParallelJob CreateJob() { // 如果任务时间较长,应该用 Allocator.Persistent MyParallelJob job = new MyParallelJob(); job.a = new NativeArray<float>(num, Allocator.TempJob); job.b = new NativeArray<float>(num, Allocator.TempJob); job.scale = 2; job.result = new NativeArray<float>(num, Allocator.TempJob); for ( int i=0; i< num; i++ ) { job.a[i] = i; job.b[i] = i; } return job; } private static void ReleaseJob(MyParallelJob job) { // 释放数组分配的内存 job.a.Dispose(); // job.b.Dispose(); 用了 [DeallocateOnJobCompletion] 就不需要手动调用了 job.result.Dispose(); } private static JobHandle ScheduleJob(MyParallelJob job) { // 调度作业,只是把作业放到队列中,并没有开始执行 // 只能在主线程调用 Schedule // 第1个参数是对象数组长度,就是总共执行几次,第2个是批次粒度,就是一个批次包含多少次执行, // 假设执行8次,批次粒度2,则分4个批次,批次0包含[0-1],批次1包含[2-3],... // 如果调用 JobHandle.ScheduleBatchedJobs() ,则会复制4个对象,然后获得4个线程分别执行4个批次 // 如果直接调用 handle.Complete(),执行时会复制3个对象,然后获得3个线程执行批次1,2,3,同时在当前线程的当前对象上执行批次0 // 也就是给cpu分配任务是按批次来的,可能分配多个批次 // 因此,执行大量计算时,特别是图像处理,经常设置较大的批次,比如 32 // 理论上,批次粒度=执行次数/cpu核心数为最优,但实际上如果次数多时,一般设置32 JobHandle handle = job.Schedule(num, 2); return handle; } // 测试执行任务 // asyncExecute 是否异步执行 public static IEnumerator TestJob(bool asyncExecute) { MyParallelJob job = CreateJob(); JobHandle handle = ScheduleJob(job); yield return ExecuteJobHandle(handle, asyncExecute); ReleaseJob(job); } // 执行作业 private static IEnumerator ExecuteJobHandle(JobHandle handle, bool asyncExecute) { if (asyncExecute) { // 异步执行 // 启动作业,所有作业都在非主线程执行 JobHandle.ScheduleBatchedJobs(); // 等待作业完成 yield return new WaitUntil(() => handle.IsCompleted); // 仍然要调用 Complete 才能访问结果 handle.Complete(); } else { // 同步执行 // 启动作业,此时也会给主线程分配作业 // 本质上只是通过增加线程来多占用cpu,如果任务太重,照样会卡,可以考虑用异步执行 // 主线程会等待任务完成后继续执行 handle.Complete(); } } }
更多推荐
所有评论(0)