参考链接 :
http://esprog.hatenablog.com/entry/2018/05/19/150313
https://blogs.unity3d.com/2018/10/22/what-is-a-job-system/
Job系统作为一个多线程系统, 它因为跟ECS有天生的融合关系所以比较重要的样子, 我也按照使用类型的分类来看看Job System到底怎么样.
1. Pure Job System
2. ECS on Job System
Job说实话就是一套封装的多线程系统, 我相信所有开发人员都能自己封装一套, 所以Unity推出这个的时候跟着ECS一起推出, 因为单独推出来的话肯定推不动, 多线程, 线程安全, 线程锁, 线程共享资源, 这些都没什么区别, 我从一个修改顶点列表的功能来说吧.
先来一个普通的多线程 :
using System.Collections; using System.Collections.Generic; using UnityEngine; using System; using System.Threading; public class NormalListAccessTest01 : MonoBehaviour { public class RunData { public List<Vector3> vecList = new List<Vector3>(); public float speed; public float deltaTime; } public static void RunOnThread<T>(System.Action<T> call, T obj, System.Action endCall = null) { System.Threading.ThreadPool.QueueUserWorkItem((_obj) => { call.Invoke(obj); if(endCall != null) { endCall.Invoke(); } }); } private void OnGUI() { if(GUI.Button(new Rect(100, 100, 100, 50), "Run Test")) { var data = new RunData(); data.deltaTime = Time.deltaTime; data.speed = 100.0f; for(int i = 0; i < 10000; i++) { data.vecList.Add(UnityEngine.Random.insideUnitSphere * 50.0f); } RunOnThread<RunData>((_data) => { Debug.Log("Start At : " + System.DateTime.Now.ToString("HH:mm:ss fff")); var move = _data.deltaTime * _data.speed; for(int i = 0; i < _data.vecList.Count; i++) { var p = _data.vecList[i] + move * Vector3.right; _data.vecList[i] = p; } }, data, () => { Debug.Log(data.vecList[0]); Debug.Log("End At : " + System.DateTime.Now.ToString("HH:mm:ss fff")); }); } } }
没有加什么锁, 简单运行没有问题, 下面来个Job的跑一下:
using UnityEngine; using Unity.Collections; using Unity.Jobs; public class JobSystemSample01 : MonoBehaviour { struct VelocityJob : IJob { public NativeArray<Vector3> position; public float deltaTime; public void Execute() { for(var i = 0; i < position.Length; i++) { position[i] = position[i] + Vector3.right * deltaTime; } } } public void Test() { var position = new NativeArray<Vector3>(100, Allocator.Persistent); var job = new VelocityJob() { deltaTime = Time.deltaTime, position = position }; JobHandle jobHandle = job.Schedule(); JobHandle.ScheduleBatchedJobs(); System.Threading.Thread.Sleep(3); Debug.Log(position[0]); // Error : You must call JobHandle.Complete() jobHandle.Complete(); Debug.Log(position[0]); position.Dispose(); } private void OnGUI() { if(GUI.Button(new Rect(100, 100, 100, 50), "Start Test")) { Test(); } } }
这里就有一个大问题了, 在有注释的地方 // Error : You must call JobHandle.Complete(), 是说在Job没有调用Complete()时, 去获取相关数组内容是非法的! 而这个jobHandle.Complete(); 无法通过工作线程去调用, 也就是说Job的运行它是无法自行结束的, 无法发出运行结束的通知的, 对比上面封装的普通多线程弱爆了. 而这个Complete()函数如果在工作线程执行完成前调用, 会强制立即执行(文档也是写 Wait for the job to complete), 也就是说它只能在主线程调用并且会阻塞主线程, 这样就可以定性了, 它的Job System不是为了提供一般使用的多线程封装给我们用的,
经过几次测试, 几乎没有办法简单扩展Job系统来让它成为像上面一样拥有自动完成通知的系统, 尝试方法如下 :
1. 添加JobHandle变量到IJob中, 在Execute结束时调用
public NativeArray<Vector3> position; public float deltaTime; [Unity.Collections.LowLevel.Unsafe.NativeDisableUnsafePtrRestriction] public JobHandle selfHandle;
报错, InvalidOperationException: VelocityJob.selfHandle.jobGroup uses unsafe Pointers which is not allowed. 无法解决
2. 添加回调函数进去
public NativeArray<Vector3> position; public float deltaTime; public System.Action endCall;
报错, Job系统的struct里面只能存在值类型!
3. 使用全局的引用以及线程转换逻辑来做成自动回调的形式, 虽然可以使用了可是非常浪费资源 :
using UnityEngine; using Unity.Collections; using Unity.Jobs; using System.Collections.Generic; public class JobSystemSample01 : MonoBehaviour { public static Dictionary<int, IJobCall> ms_handleRef = new Dictionary<int, IJobCall>(); public class IJobCall { public JobHandle jobHandle; public System.Action endCall; } struct VelocityJob : IJob { public NativeArray<Vector3> position; public float deltaTime; public int refID; public void Execute() { for(var i = 0; i < position.Length; i++) { position[i] = position[i] + Vector3.right * deltaTime; } var handle = ms_handleRef[refID]; ThreadMaster.Instance.CallFromMainThread(() => { handle.jobHandle.Complete(); if(handle.endCall != null) { handle.endCall.Invoke(); } }); } } public void Test() { ThreadMaster.GetOrCreate(); var position = new NativeArray<Vector3>(100, Allocator.Persistent); var job = new VelocityJob() { deltaTime = 1f, position = position, refID = 1, }; ms_handleRef[1] = new IJobCall() { jobHandle = job.Schedule(), endCall = () => { Debug.Log(position[0]); position.Dispose(); } }; } private void OnGUI() { if(GUI.Button(new Rect(100, 100, 100, 50), "Start Test")) { Test(); } } }
转换线程就用简单的回调 :
using System.Collections; using System.Collections.Generic; using UnityEngine; public class ThreadMaster : MonoBehaviour { private static ThreadMaster _instance; public static ThreadMaster Instance { get { return GetOrCreate(); } } private volatile List<System.Action> _calls = new List<System.Action>(); public static ThreadMaster GetOrCreate() { if(_instance == false) { _instance = new GameObject("ThreadMaster").AddComponent<ThreadMaster>(); } return _instance; } public void CallFromMainThread(System.Action call) { _calls.Add(call); } void Update() { if(_calls.Count > 0) { for(int i = 0; i < _calls.Count; i++) { var call = _calls[i]; call.Invoke(); } _calls.Clear(); } } }
所以按照原理上来看, Job并不是为了一般化的多线程而做的简单封装, 反而它的逻辑相当复杂, 因为如果用在ECS系统上的话, 比如在计算人物骨骼动画, 蒙皮等数据的过程中, 到了渲染步骤, 它就必须在渲染之前强制完成所有计算来保证渲染的正确性吧, 它会把线程提到最高或者强制转换到主线程也有可能, 因为之前我用过转换上下文的一个框架(应该是使用Async语法代替协程的那个框架), 所以说它是为ECS服务的也不为过.
通过上面封装就可以作为一般多线程使用了, 并且我们获得了引擎提供的数据安全和高效逻辑性, 再加上利用BurstCpmpile和只读属性, 能够提升一些计算效率吧. ECS on Job已经在另外一篇中说过了, 这里忽略了.
原文:https://www.cnblogs.com/tiancaiwrk/p/12410825.html