unity中使用Job

  • 效率最高,unity 专门开辟了一个线程池来执行 Job,用这个执行多线程任务效率最高

    • 首先直接用线程池中的线程,省掉线程分配和释放
    • 其次会根据cpu数量分配合适的线程数量,避免恶性竞争cpu,产生过多的上下文切换
  • 应用场景

    • 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();
    			}
    		}
    	}
    
Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐