新钥匙建站,阿里企业邮箱个人登录,wordpress手机不兼容,小程序定制一般多少钱概述
设计目的#xff1a;简单安全地使用多线程#xff0c;随便就能写出高性能代码
收益#xff1a;FPS更高#xff0c;电池消耗更低#xff08;Burst编译器#xff09;
并行性#xff1a;C# Job System和Unity Native Job System共享工作线程worker threads#xf…概述
设计目的简单安全地使用多线程随便就能写出高性能代码
收益FPS更高电池消耗更低Burst编译器
并行性C# Job System和Unity Native Job System共享工作线程worker threads也就是它们不会创建超过CPU cores数量的线程也就不会导致CPU资源抢占问题。
什么是多线程
单线程一次执行一条指令产生一个结果
多线程利用CPU的多核多条指令同时执行其他线程执行完成后会将结果同步给主线程。
多线程好的实践几个运行时间很长的任务。
游戏代码的特点大量小而短的任务。
解决方案线程池。
context switching线程上下文切换性能敏感的要尽量避免。 当激活的线程数超过CPU cores时就会导致CPU资源争夺从而触发频繁的context switching。 过程先saving执行了一部分的当前线程然后执行另外的线程切回来的时候再reconstructing之前的线程再继续执行。
什么是Job System
简化多线程job system通过创建jobs来实现多线程而不是直接创建thread。
job概念完成特定任务的一个小的工作单元。job接收参数并操作数据类似于函数调用。job之间可以有依赖关系也就是一个job可以等另一个job完成之后再执行。
job system管理一组worker threads并且保证一个logical CPU core一个worker thread避免context switching。
job system将jobs放在一个job queue里面worker threads从job queue里面获取job然后执行。
job依赖性job system管理job依赖关系并保证执行时序的正确性。
C# Job System的Safety System
Race conditions竞争条件一个输出结果依赖于不受控制的事件出现的顺序或时机。
在写多线程代码时race conditions是一个很大的挑战。race conditions不是bug但它会导致不确定性行为。并且一旦出现就很难定位也很难调试因为它依赖时机打断点和加log本身都会改变各个独立线程执行的时机。
Safety system为了写出更安全的多线程代码C# Job System会检查所有的潜在的race conditions并保护代码不受可能会产生的bug的影响这句话有点模糊......。
解决办法数据拷贝每个job操作来自主线程数据的副本而不是操作原数据。这样数据独立就不会产生race conditions了。
blittable data typesjob只能访问blittable的数据这些数据在托管代码和native代码之间拷贝的时候不需要做额外的类型转换。
拷贝方式memcpy
NativeContainer
NativeContainer实际上是native memory的一个wrapper包含一个指向非托管内存的指针。
不需要拷贝使用NativeContainer可以让一个job和main thread共享数据而不用拷贝。copy虽然能保证Safety System但每个job的计算结果也是分开的。
可使用的C#类型定义 数据结构说明来源NativeArray数组UnityNativeSlice可以访问一个NativeArray的某一部分UnityNativeList一个可变长的NativeArrayECSNativeHashMapkey value pairsECSNativeMultiHashMap一个key对应多个valuesECSNativeQueueFIFO的queueECS
Safety System安全策略 Safety System内置于所有的NativeContainer会自动跟踪NativeContainer的读写状态。 注意所有的safety checkes都只在Editor和PlayMode模式下生效bounds checks、deallocation checks、race condition checks。 还有一部分安全策略 DisposeSentinel自动检测memory leak并报错。依赖宏定义ENABLE_UNITY_COLLECTIONS_CHECKS。 AtomicSafetyHandle用来转移NativeContainer的控制权。比如当2个jobs同时写一个NativeContainerSafety System就会抛出一个error并描述如何解决。异常会在产生冲突的job调度时抛出。依赖宏定义ENABLE_UNITY_COLLECTIONS_CHECKS。 这种情况下可以使用job依赖让其中一个job依赖另外一个job的完成。
规则Safety System允许多个job同时read同一块数据。
规则Safety System不允许一个job正在writing数据时调度激活另一个“拥有write权限”的job不是不让同时write。
规则手动指定job对数据的只读默认是可读写会影响性能 [ReadOnly]public NativeArrayint input; 注意job对static data的访问没有Safety System安全保护所以使用不当可能造成crash。 NativeContainer Allocator分配器
1Allocator.Temp 最快维持1 framejob不能用需要手动Dispose()比如可以再native层的callback调用时使用。
2Allocator.TempJbo 稍微慢一点最多维持4 framesthread-safe如果4 frames内没有Dispose()会有warning。大多数small jobs都会使用这个类型的分配器.
3Allocator.Persistent 最慢但是可持久存在就是malloc的wrapper。Longer jobs使用这个类型但在性能敏感的地方不应该使用。
NativeArrayfloat result new NativeArrayfloat(1, Allocator.TempJob);
创建Job
三要素
1创建一个struct实现接口IJob
2添加数据成员要么是blittable类型 要么是NativeContainer
3添加Execute()方法实现。
执行job时job.Execute()方法会在一个cpu core上执行一次。
注意job操作数据是基于拷贝的除非是NativeContainer类型。那么一个job访问main thread数据的唯一方式就是使用NativeContainer。 public struct TestJob : IJob
{public float a;public float b;public NativeArrayfloat result;public void Execute(){result[0] a b;}
} 调度Job
三要素
1实例化job
2设置数据
3调用job.Schedule()方法。
调用Schedule方法会将job放到job queue里面等待执行。一旦开始schedule就没法中断job了。疑问这个once scheduled是job.Schedule方法还是从job queue里面拿出来开始执行 private void TestScheduleJob()
{// Create a native array of a single float to store the result. This example waits for the job to complete for illustration purposesNativeArrayfloat result new NativeArrayfloat(1, Allocator.TempJob);// Set up the job dataMyJob jobData new MyJob();jobData.a 10;jobData.b 10;jobData.result result;// Schedule the jobJobHandle handle jobData.Schedule();// Wait for the job to completehandle.Complete();// All copies of the NativeArray point to the same memory, you can access the result in your copy of the NativeArrayfloat aPlusB result[0];// Free the memory allocated by the result arrayresult.Dispose();
} JobHandle和Job依赖
设置job依赖关系
JobHandle firstJobHandle firstJob.Schedule();
secondJob.Schedule(firstJobHandle);
secondJob依赖firstJob的结果。
组合依赖项
NativeArrayJobHandle handles new NativeArrayJobHandle(numJobs, Allocator.TempJob);
// Populate handles with JobHandles from multiple scheduled jobs...
JobHandle jh JobHandle.CombineDependencies(handles);
在main thread中等待jobs执行完成 flush job使用JobHandle.Complete()来等待job执行完成。 job只有Schedule之后才会执行如果你想在main thread中访问job的正在使用的数据你可以调用JohHandle.Comlete()。该方法flush job并开始执行然后将NativeContainer的数据权限返回给main thread。 如果你不需要访问数据也可以调用统一static flush函数JobHandle.ScheduleBatchedJobs()当然该方法会影响到性能。 public struct MyJob : IJob
{public float a;public float b;public NativeArrayfloat result;public void Execute(){result[0] a b;}
}
public struct AddOneJob : IJob
{public NativeArrayfloat result;public void Execute(){result[0] result[0] 1;}
}private void TestScheduleJob()
{NativeArrayfloat result new NativeArrayfloat(1, Allocator.TempJob);MyJob jobData new MyJob();jobData.a 10;jobData.b 10;jobData.result result;JobHandle firstHandle jobData.Schedule();AddOneJob incJobData new AddOneJob();incJobData.result result;JobHandle secondHandle incJobData.Schedule(firstHandle);secondHandle.Complete();float aPlusB result[0];result.Dispose();
} ParallelFor jobs 并行job
IJob只能一次一个job执行一个任务但游戏开发中经常需要重复执行某个动作很多次这时候就可以用到并行任务IJobParallelFor。 ParallelFor jobs使用NativeArray作为数据源并且运行在多个core上还是一个job一个core只是每个job只负责处理完整数据的一个子集。 Execute(idx)方法对于数据源NativeArray中的每个item都调用一次。 调度 需要手动指定执行次数表示需要分多少次独立Execute来执行一般直接取NativeArray的数组长度作为执行次数一次处理一个数据。 当一个native job提前完成它的batches它会从其他的native job偷取一部分batches然后继续执行。
颗粒度问题分得太细会有work不断重建的开销分得太粗又会有单核负载问题。
尝试法所以最佳实践是从1开始逐步增加直到性能不再提高。 public struct MyParallelJob : IJobParallelFor
{public NativeArrayfloat a;public NativeArrayfloat b;public NativeArrayfloat result;public void Execute(int index){result[index] a[index] b[index];}
}private void TestScheduleParallelJob()
{NativeArrayfloat a new NativeArrayfloat(10, Allocator.TempJob);NativeArrayfloat b new NativeArrayfloat(10, Allocator.TempJob);NativeArrayfloat result new NativeArrayfloat(10, Allocator.TempJob);for(int i 0; i 10; i){a[i] i * 0.3f;b[i] i * 0.5f;}MyParallelJob jobData new MyParallelJob();jobData.a a;jobData.b b;jobData.result result;JobHandle handle jobData.Schedule(10, 1);handle.Complete();for(int i 0; i 10; i){Debug.LogError(result[i]);}a.Dispose();b.Dispose();result.Dispose();
} ParallelForTransform jobs
public struct MyTransformParallelJob : IJobParallelForTransform
{public void Execute(int index, TransformAccess transform){}
}
注意事项
1不能在job中访问static数据 在job中访问static数据是没有Safety System保证的可能会导致crash。unity后续版本会增加static analysis来阻止这种用法。 2Flush scheduled batchs JobHandle.ScheduleBatchedJobs当你想要你的job开始执行是可以调用这个函数flush调度的batch。 不flush batch会导致调度延迟到主线程等待batch执行结果时才触发执行。 JobHandle.Complete直接开始执行。 在ECS中batch flush是隐式执行的不需要手动调用JobHandle.ScheduleBatchJobs。 3不要试图更新NativeContainer的内容 因为缺乏ref returns机制所以不要这样用 nativeArray[0];// 等同于var tmp nativeArray[0];tmp;// 不生效// 正确的写法是var tmp nativeArray[0];tmp;nativeArray[0] tmp;MyStruct temp myNativeArray[i]; temp.memberVariable 0;myNativeArray[i] temp; 4调用JobHandle.Complete来让main thread重获控制权 主线程在访问数据之前需要依赖的job调用complete。不能只是check JobHandle.IsCompleted而是需要手动调用JobHandle.Complete()。 此调用还会清理Safety System的状态不调用的话会有内存泄漏。 5在主线程中使用Schedule和Complete 这两个函数只能在主线程中调用。不能因为一个job依赖另一个job就在前一个job中手动schedule另一个job。 6在正确的时间使用Schedule和Complete Schedule在数据填充完毕立马调用 Complete只在你需要result的时候调用 7NativeContainer添加read-only标记 默认是可读写的如果确定只读就标记为read-only可以提升性能。 8检查数据依赖 如果在profiler里看到main thread有“WaitForJobGroup”就表示在等待worker thread处理完成。也就是说你的代码里面在什么地方引入了一个data dependency这时候可以通过检查JobHandle.Complete来看一下是什么依赖关系导致了main thread需要等待的情况。 9调试jobs Jobs有一个Run函数你可以用它来替换原本调用Schedule的地方从而在main thread上立即执行这个job。可以使用这个方法来调试。 10不要在job里面分配托管内存managed memory 在job里面分配托管内存是非常慢的而且会导致Burst compiler没法使用。 Burst是基于LLVM的后端编译技术它可以利用平台特定能力将c# jobs代码编译成高度优化过的机器码。 Unity GDC 2018: C# Job System
https://www.youtube.com/playlist?listPLX2vGYjWbI0RuXtGMYKqChoZC2b-H4tck Unity at GDC - Job System Entity Component System
https://www.youtube.com/watch?vkwnb9Clh2Ist1s Job System介绍
http://www.pianshen.com/article/634466006/