Qué son Async, Await y Task en C#

El rendimiento lo es todo cuando intenta publicar en la web, dispositivos móviles, consolas e incluso algunas de las PC de gama baja. Un juego o aplicación que se ejecuta a menos de 30 FPS puede causar frustración a los usuarios. Echemos un vistazo a algunas de las cosas que podemos usar para aumentar el rendimiento al reducir la carga en la CPU.

En esta publicación, cubriremos qué son async, awaity Tasken C# y cómo usarlos en Unity para obtener rendimiento en su proyecto. A continuación, veremos algunos de los paquetes integrados de Unity: rutinas, el sistema de trabajo de C# y el compilador de ráfagas. Veremos qué son, cómo usarlos y cómo aumentan el rendimiento en su proyecto.

Para comenzar este proyecto, usaré Unity 2021.3.4f1. No he probado este código en ninguna otra versión de Unity; todos los conceptos aquí deberían funcionar en cualquier versión de Unity posterior a Unity 2019.3. Sus resultados de rendimiento pueden diferir si usa una versión anterior, ya que Unity realizó algunas mejoras significativas con el modelo de programación async/await en 2021. Obtenga más información al respecto en el blog de Unity Unity and .NET, what's next , en particular, la sección denominada "Modernizing the Tiempo de ejecución de la unidad”.

Creé un nuevo proyecto Core 2D (URP), pero puede usarlo en cualquier tipo de proyecto que desee.

Tengo un sprite que obtuve de Space Shooter (Redux, además de fuentes y sonidos) de Kenney Vleugels .

Creé un prefabricado enemigo que contiene un Sprite Render y un Enemy Component. El Enemy Component es un MonoBehaviourque tiene a Transformy a floatpara realizar un seguimiento de la posición y la velocidad para moverse en el eje y:

using UnityEngine;

public class Enemy
{
   public Transform transform;
   public float moveY;
}

Qué async, awaity Taskson en C#

¿Qué es async?

En C#, los métodos pueden tener una asyncpalabra clave delante de ellos, lo que significa que los métodos son métodos asincrónicos. Esta es solo una forma de decirle al compilador que queremos poder ejecutar el código interno y permitir que la persona que llama a ese método continúe con la ejecución mientras espera que finalice este método.

Un ejemplo de esto sería cocinar una comida. Comenzarás a cocinar la carne, y mientras la carne se cocina y esperas a que termine, comenzarás a hacer los lados. Mientras se cocina la comida, empezarías a poner la mesa. Un ejemplo de esto en código sería static async Task<Steak> MakeSteak(int number).

Unity también tiene todo tipo de métodos incorporados a los que puede llamar de forma asíncrona; consulte los documentos de Unity para obtener una lista de métodos. Con la forma en que Unity maneja la administración de la memoria, utiliza corrutinas oAsyncOperation el sistema de trabajo de C# .

¿Qué es awaity cómo se usa?

En C#, puede esperar a que se complete una operación asíncrona usando la awaitpalabra clave. Esto se usa dentro de cualquier método que tenga la asyncpalabra clave para esperar a que continúe una operación:

Public async void Update()
{
     // do stuff
     await // some asynchronous method or task to finish
     // do more stuff or do stuff with the data returned from the asynchronous task.
}

Consulte los documentos de Microsoft para obtener más información sobre await.

¿Qué es un Tasky cómo se usa?

A Taskes un método asíncrono que realiza una única operación y no devuelve ningún valor. Para a Taskque devuelve un valor, usaríamos Task<TResult>.

Para usar una tarea, la creamos como crear cualquier objeto nuevo en C#: Task t1 = new Task(void Action). A continuación, comenzamos la tarea t1.wait. Por último, esperamos a que la tarea se complete con t1.wait.

Hay varias formas de crear, iniciar y ejecutar tareas. Task t2 = Task.Run(void Action)creará y comenzará una tarea. await Task.Run(void Action)creará, iniciará y esperará a que se complete la tarea. Podemos usar la forma alternativa más común con Task t3 = Task.Factory.Start(void Action).

Hay varias formas en que podemos esperar a que se complete la tarea. int index = Task.WaitAny(Task[])esperará a que se complete cualquier tarea y nos dará el índice de la tarea completada en la matriz. await Task.WaitAll(Task[])esperará a que se completen todas las tareas.

Para obtener más información sobre las tareas, consulte los documentos de Microsoft .

un ejemplo sencillotask

private void Start()
{
   Task t1 = new Task(() => Thread.Sleep(1000));
   Task t2 = Task.Run(() => Thread.Sleep(2000000));
   Task t3 = Task.Factory.StartNew(() => Thread.Sleep(1000));
   t1.Start();
   Task[] tasks = { t1, t2, t3 };
   int index = Task.WaitAny(tasks);
   Debug.Log($"Task {tasks[index].Id} at index {index} completed.");

   Task t4 = new Task(() => Thread.Sleep(100));
   Task t5 = Task.Run(() => Thread.Sleep(200));
   Task t6 = Task.Factory.StartNew(() => Thread.Sleep(300));
   t4.Start();
   Task.WaitAll(t4, t5, t6);
   Debug.Log($"All Task Completed!");
   Debug.Log($"Task When any t1={t1.IsCompleted} t2={t2.IsCompleted} t3={t3.IsCompleted}");
   Debug.Log($"All Task Completed! t4={t4.IsCompleted} t5={t5.IsCompleted} t6={t6.IsCompleted}");
}

public async void Update()
{
   float startTime = Time.realtimeSinceStartup;
   Debug.Log($"Update Started: {startTime}");
   Task t1 = new Task(() => Thread.Sleep(10000));
   Task t2 = Task.Run(() => Thread.Sleep(20000));
   Task t3 = Task.Factory.StartNew(() => Thread.Sleep(30000));

   await Task.WhenAll(t1, t2, t3);
   Debug.Log($"Update Finished: {(Time.realtimeSinceStartup - startTime) * 1000f} ms");
}

Cómo afecta la tarea al rendimiento

Ahora comparemos el rendimiento de una tarea con el rendimiento de un método.

Necesitaré una clase estática que pueda usar en todas mis comprobaciones de rendimiento. Tendrá un método y una tarea que simule una operación intensiva en rendimiento. Tanto el método como la tarea realizan exactamente la misma operación:

using System.Threading.Tasks;
using Unity.Mathematics;

public static class Performance
{
   public static void PerformanceIntensiveMethod(int timesToRepeat)
   {
       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < timesToRepeat; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }

   public static Task PerformanceIntensiveTask(int timesToRepeat)
   {
       return Task.Run(() =>
       {
           // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
           float value = 0f;
           for (int i = 0; i < timesToRepeat; i++)
           {
               value = math.exp10(math.sqrt(value));
           }
       });
   }
}

Ahora necesito uno MonoBehaviourque pueda usar para probar el impacto en el rendimiento de la tarea y el método. Solo para poder ver un mejor impacto en el rendimiento, fingiré que quiero ejecutar esto en diez objetos de juego diferentes. También realizaré un seguimiento de la cantidad de tiempo Updateque tarda en ejecutarse el método.

En Update, obtengo la hora de inicio. Si estoy probando el método, recorro todos los objetos del juego simulado y llamo al método intensivo en rendimiento. Si estoy probando la tarea, creo un nuevo Taskbucle de matriz a través de todos los objetos del juego simulado y agrego la tarea de rendimiento intensivo a la matriz de tareas. Entonces awaitpara todas las tareas para completar. Fuera de la verificación del tipo de método, actualizo el tiempo del método, convirtiéndolo a ms. También lo registro.

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task
   }

   [SerializeField] private int numberGameObjectsToImitate
= 10;

   [SerializeField] private MethodType method = MethodType.Normal;

   [SerializeField] private float methodTime;

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           case MethodType.Normal:
               for (int i = 0; i < numberGameObjectsToImitate
; i++)
                   Performance.PerformanceIntensiveMethod(50000);
               break;
           case MethodType.Task:
               Task[] tasks = new Task[numberGameObjectsToImitate
];
               for (int i = 0; i < numberGameObjectsToImitate
; i++)
                   tasks[i] = Performance.PerformanceIntensiveTask(5000);
               await Task.WhenAll(tasks);
               break;
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
   }
}

El método intensivo tarda alrededor de 65 ms en completarse y el juego se ejecuta a unos 12 FPS.

La tarea intensiva tarda alrededor de 4 ms en completarse y el juego se ejecuta a unos 200 FPS.

Intentemos esto con mil enemigos:

using System.Collections.Generic;
using System.Threading.Tasks;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
using Random = UnityEngine.Random;

public class PerformanceTaskJob : MonoBehaviour
{
   private enum MethodType
   {
       NormalMoveEnemy,
       TaskMoveEnemy
   }

   [SerializeField] private int numberEnemiesToCreate = 1000;
   [SerializeField] private Transform pfEnemy;

   [SerializeField] private MethodType method = MethodType.NormalMoveEnemy;
   [SerializeField] private float methodTime;

   private readonly List<Enemy> m_enemies = new List<Enemy>();

   private void Start()
   {
       for (int i = 0; i < numberEnemiesToCreate; i++)
       {
           Transform enemy = Instantiate(pfEnemy,
                                         new Vector3(Random.Range(-8f, 8f), Random.Range(-8f, 8f)),
                                         Quaternion.identity);
           m_enemies.Add(new Enemy { transform = enemy, moveY = Random.Range(1f, 2f) });
       }
   }

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           case MethodType.NormalMoveEnemy:
               MoveEnemy();
               break;
           case MethodType.TaskMoveEnemy:
               Task<Task[]> moveEnemyTasks = MoveEnemyTask();
               await Task.WhenAll(moveEnemyTasks);
               break;
           default:
               MoveEnemy();
               break;
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
   }

   private void MoveEnemy()
   {
       foreach (Enemy enemy in m_enemies)
       {
           enemy.transform.position += new Vector3(0, enemy.moveY * Time.deltaTime);
           if (enemy.transform.position.y > 5f)
               enemy.moveY = -math.abs(enemy.moveY);
           if (enemy.transform.position.y < -5f)
               enemy.moveY = +math.abs(enemy.moveY);
           Performance.PerformanceIntensiveMethod(1000);
       }
   }

   private async Task<Task[]> MoveEnemyTask()
   {
       Task[] tasks = new Task[m_enemies.Count];
       for (int i = 0; i < m_enemies.Count; i++)
       {
           Enemy enemy = m_enemies[i];
           enemy.transform.position += new Vector3(0, enemy.moveY * Time.deltaTime);
           if (enemy.transform.position.y > 5f)
               enemy.moveY = -math.abs(enemy.moveY);
           if (enemy.transform.position.y < -5f)
               enemy.moveY = +math.abs(enemy.moveY);
           tasks[i] = Performance.PerformanceIntensiveTask(1000);
       }

       await Task.WhenAll(tasks);

       return tasks;
  }

Mostrar y mover mil enemigos con el método tomó alrededor de 150 ms con una velocidad de cuadro de aproximadamente 7 FPS.

Mostrar y mover mil enemigos con una tarea tomó alrededor de 50 ms con una velocidad de cuadro de aproximadamente 30 FPS.

¿Por qué no useTasks?

Las tareas son extremadamente eficientes y reducen la tensión en el rendimiento de su sistema. Incluso puede usarlos en múltiples subprocesos usando la Biblioteca paralela de tareas (TPL).

Sin embargo, existen algunos inconvenientes al usarlos en Unity. El principal inconveniente de usar TaskUnity es que todos se ejecutan en el Mainsubproceso. Sí, podemos hacer que se ejecuten en otros subprocesos, pero Unity ya realiza su propia gestión de subprocesos y memoria, y puede crear errores al crear más subprocesos que núcleos de CPU, lo que genera competencia por los recursos.

Las tareas también pueden ser difíciles de realizar correctamente y depurar. Al escribir el código original, terminé con todas las tareas ejecutándose, pero ninguno de los enemigos se movió en la pantalla. Terminó siendo que necesitaba devolver el Task[]que creé en el archivo Task.

Las tareas crean mucha basura que afecta el rendimiento. Tampoco aparecen en el generador de perfiles, por lo que si tiene uno que afecta el rendimiento, es difícil rastrearlo. Además, he notado que a veces mis tareas y funciones de actualización continúan ejecutándose desde otras escenas.

Corrutinas de Unity

Según Unity , "una corrutina es una función que puede suspender su ejecución (rendimiento) hasta que finalice la instrucción de rendimiento dada " .

Lo que esto significa es que podemos ejecutar código y esperar a que se complete una tarea antes de continuar. Esto es muy parecido a un método asíncrono. Utiliza un tipo de retorno IEnumeratory we yield returnen lugar de await.

Unity tiene varios tipos diferentes de instrucciones de rendimiento que podemos usar, es decir, WaitForSeconds, WaitForEndOfFrame, WaitUntilo WaitWhile.

Para iniciar las corrutinas, necesitamos MonoBehavioury usamos el MonoBehaviour.StartCoroutine.

Para detener una rutina antes de que se complete, usamos MonoBehaviour.StopCoroutine. Al detener las rutinas, asegúrese de utilizar el mismo método que utilizó para iniciarlas.

Los casos de uso comunes para corrutinas en Unity son esperar a que se carguen los activos y crear temporizadores de enfriamiento.

Ejemplo: un cargador de escenas usando una corrutina

using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;

public class SceneLoader : MonoBehaviour
{
   public Coroutine loadSceneCoroutine;
   public void Update()
   {
       if (Input.GetKeyDown(KeyCode.Space) && loadSceneCoroutine == null)
       {
           loadSceneCoroutine = StartCoroutine(LoadSceneAsync());
       }

       if (Input.GetKeyDown(KeyCode.Escape) && loadSceneCoroutine != null)
       {
           StopCoroutine(loadSceneCoroutine);
           loadSceneCoroutine = null;
       }
   }

   private IEnumerator LoadSceneAsync()
   {
       AsyncOperation asyncLoad = SceneManager.LoadSceneAsync("Scene2");
       yield return new WaitWhile(() => !asyncLoad.isDone);
   }
}

Comprobar el impacto de una rutina en el rendimiento

Veamos cómo el uso de una corrutina afecta el rendimiento de nuestro proyecto. Solo voy a hacer esto con el método intensivo en rendimiento.

Agregué Coroutinea la MethodTypeenumeración y las variables para realizar un seguimiento de su estado:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task,
       Coroutine
   }

   ...

   private Coroutine m_performanceCoroutine;

Creé la rutina. Esto es similar a la tarea y el método de alto rendimiento que creamos anteriormente con código agregado para actualizar el tiempo del método:

   private IEnumerator PerformanceCoroutine(int timesToRepeat, float startTime)
   {
       for (int count = 0; count < numberGameObjectsToImitate; count++)
       {
           // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
           float value = 0f;
           for (int i = 0; i < timesToRepeat; i++)
           {
               value = math.exp10(math.sqrt(value));
           }
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
       m_performanceCoroutine = null;
       yield return null;
   }

En el Updatemétodo, agregué el cheque para la rutina. También modifiqué el tiempo del método, actualicé el código y agregué código para detener la rutina si se estaba ejecutando y cambiamos el tipo de método:

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Coroutine:
               m_performanceCoroutine ??= StartCoroutine(PerformanceCoroutine(5000, startTime));
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       if (method != MethodType.Coroutine)
       {
           methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
           Debug.Log($"{methodTime} ms");
       }

       if (method != MethodType.Coroutine || m_performanceCoroutine == null) return;
       StopCoroutine(m_performanceCoroutine);
       m_performanceCoroutine = null;
   }

La rutina intensiva tarda alrededor de 6 ms en completarse y el juego se ejecuta a unos 90 FPS.

El sistema de trabajos de C# y el compilador de ráfagas

¿Qué es el sistema de trabajo de C#?

El sistema de trabajo de C# es la implementación de Unity de tareas que son fáciles de escribir, no generan la basura que generan las tareas y utilizan los subprocesos de trabajo que Unity ya ha creado. Esto corrige todas las desventajas de las tareas.

Unity compara los trabajos como subprocesos, pero dicen que un trabajo realiza una tarea específica. Los trabajos también pueden depender de otros trabajos para completarse antes de ejecutarse; esto soluciona el problema con la tarea que tenía que no se movió correctamente Unitsporque dependía de que otra tarea se completara primero.

Unity se ocupa automáticamente de las dependencias laborales. El sistema de trabajo también tiene un sistema de seguridad incorporado principalmente para proteger contra las condiciones de carrera . Una advertencia con los trabajos es que solo pueden contener variables miembro que sean tipos blittables o tipos NativeContainer ; esto es un inconveniente del sistema de seguridad.

Para usar el sistema de trabajos, cree el trabajo, programe el trabajo, espere a que se complete y luego use los datos devueltos por el trabajo. El sistema de trabajo es necesario para usar la pila de tecnología orientada a datos (DOTS) de Unity.

Para obtener más detalles sobre el sistema de trabajo, consulte la documentación de Unity .

Creando un trabajo

Para crear un trabajo, cree uno stuctque implemente una de las IJobinterfaces ( IJob , IJobFor , IJobParallelFor , Unity.Engine.Jobs.IJobParallelForTransform ). IJobes un trabajo básico. IJobFory IJobForParallelse utilizan para realizar la misma operación en cada elemento de un contenedor nativo o para varias iteraciones. La diferencia entre ellos es que IJobFor se ejecuta en un solo subproceso donde IJobForParallelse dividirá entre varios subprocesos.

Lo usaré IJobpara crear un trabajo de operación intensiva IJobFory IJobForParallelpara crear un trabajo que moverá a múltiples enemigos; esto es solo para que podamos ver los diferentes impactos en el rendimiento. Estos trabajos serán idénticos a las tareas y métodos que creamos anteriormente:

public struct PerformanceIntensiveJob : IJob { }
public struct MoveEnemyJob: IJobFor { }
public struct MoveEnemyParallelJob : IJobParallelFor { }

Agregue las variables miembro. En mi caso, mi IJobno necesita ninguno. Los IJobFory IJobParallelFornecesitan un valor flotante para el tiempo delta actual, ya que los trabajos no tienen un concepto de marco; operan fuera de Unity MonoBehaviour. También necesitan una matriz float3para la posición y una matriz para la velocidad de movimiento en el eje y:

public struct MoveEnemyJob : IJobFor
{
   public NativeArray<float3> positions;
   public NativeArray<float> moveYs;
   public float deltaTime; 
}
public struct MoveEnemyParallelJob : IJobParallelFor
{
   public NativeArray<float3> positions;
   public NativeArray<float> moveYs;
   public float deltaTime;
}

El último paso es implementar el Executemétodo requerido. Los IJobFory IJobForParallelrequieren un intpara el índice de la iteración actual que se está ejecutando el trabajo.

La diferencia es que en lugar de acceder a los movimientos del enemigo transform, usamos la matriz que está en el trabajo:

public struct PerformanceIntensiveJob : IJob
{
   #region Implementation of IJob

   /// <inheritdoc />
   public void Execute()
   {
       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < 50000; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }

   #endregion
}

// MoveEnemyJob and MoveEnemyParallelJob have the same exact Execute Method. 

   /// <inheritdoc />
   public void Execute(int index)
   {
       positions[index] += new float3(0, moveYs[index] * deltaTime, 0);
       if (positions[index].y > 5f)
           moveYs[index] = -math.abs(moveYs[index]);
       if (positions[index].y < -5f)
           moveYs[index] = +math.abs(moveYs[index]);

       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < 1000; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }   private JobHandle PerformanceIntensiveMethodJob()
   {
       PerformanceIntensiveJob job = new PerformanceIntensiveJob();
       return job.Schedule();
   }

Programar un trabajo

Primero, necesitamos instalar el trabajo y completar los datos del trabajo:

NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);

MyJob jobData = new MyJob();
jobData.myFloat = result;

Luego programamos el trabajo con JobHandle jobHandle = jobData.Schedule();. El Schedulemétodo devuelve un JobHandleque se puede utilizar más adelante.

No podemos programar un trabajo desde dentro de un trabajo. Sin embargo, podemos crear nuevos trabajos y completar sus datos desde dentro de un trabajo. Una vez que se ha programado un trabajo, no se puede interrumpir.

El trabajo intensivo en rendimiento

Creé un método que crea un nuevo trabajo y lo programa. Devuelve el identificador de trabajo que puedo usar en mi updatemétodo:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   ....

   private JobHandle PerformanceIntensiveMethodJob()
   {
       PerformanceIntensiveJob job = new PerformanceIntensiveJob();
       return job.Schedule();
   }
}

Agregué el trabajo a mi enumeración. Luego, en el Updatemétodo, agrego el casea la switchsección. Creé una matriz de JobHandles. Luego recorro todos los objetos del juego simulado y agrego un trabajo programado para cada uno a la matriz:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task,
       Coroutine,
       Job
   }

   ...
   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Job:
               NativeArray<JobHandle> jobHandles =
                   new NativeArray<JobHandle>(numberGameObjectsToImitate, Allocator.Temp);
               for (int i = 0; i < numberGameObjectsToImitate; i++)
                   jobHandles[i] = PerformanceIntensiveMethodJob();
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       ...
   }
}

el MoveEnemyyMoveEnemyParallelJob

A continuación, agregué los trabajos a mi enumeración. Luego, en el Updatemétodo, llamo a un nuevo MoveEnemyJobmétodo, pasando el tiempo delta. Normalmente usaría el JobForo el JobParallelFor:

public class PerformanceTaskJob : MonoBehaviour
{
   private enum MethodType
   {
       NormalMoveEnemy,
       TaskMoveEnemy,
       MoveEnemyJob,
       MoveEnemyParallelJob
   }

   ...

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.MoveEnemyJob:
           case MethodType.MoveEnemyParallelJob:
               MoveEnemyJob(Time.deltaTime);
               break;
           default:
               MoveEnemy();
               break;
       }

       ...
   }

   ...

Lo primero que hago es establecer una matriz para las posiciones y una matriz para las moveYque pasaré a los trabajos. Luego lleno estas matrices con los datos de los enemigos. A continuación, creo el trabajo y configuro los datos del trabajo según el trabajo que quiero usar. Después de eso, programo el trabajo según el trabajo que quiero usar y el tipo de programación que quiero hacer:

private void MoveEnemyJob(float deltaTime)
   {
       NativeArray<float3> positions = new NativeArray<float3>(m_enemies.Count, Allocator.TempJob);
       NativeArray<float> moveYs = new NativeArray<float>(m_enemies.Count, Allocator.TempJob);

       for (int i = 0; i < m_enemies.Count; i++)
       {
           positions[i] = m_enemies[i].transform.position;
           moveYs[i] = m_enemies[i].moveY;
       }

       // Use one or the other
       if (method == MethodType.MoveEnemyJob)
       {
           MoveEnemyJob job = new MoveEnemyJob
           {
               deltaTime = deltaTime,
               positions = positions,
               moveYs = moveYs
           };

           // typically we would use one of these methods
           switch (moveEnemyJobType)
           {
               case MoveEnemyJobType.ImmediateMainThread:
                   // Schedule job to run immediately on main thread.
                   // typically would not use.
                   job.Run(m_enemies.Count);
                   break;
               case MoveEnemyJobType.ScheduleSingleWorkerThread:
               case MoveEnemyJobType.ScheduleParallelWorkerThreads:
               {
                   // Schedule job to run at a later point on a single worker thread.
                   // First parameter is how many for-each iterations to perform.
                   // The second parameter is a JobHandle to use for this job's dependencies.
                   //   Dependencies are used to ensure that a job executes on worker threads after the dependency has completed execution.
                   //   In this case we don't need our job to depend on anything so we can use a default one.
                   JobHandle scheduleJobDependency = new JobHandle();
                   JobHandle scheduleJobHandle = job.Schedule(m_enemies.Count, scheduleJobDependency);

                   switch (moveEnemyJobType)
                   {
                       case MoveEnemyJobType.ScheduleSingleWorkerThread:
                           break;
                       case MoveEnemyJobType.ScheduleParallelWorkerThreads:
                       {
                           // Schedule job to run on parallel worker threads.
                           // First parameter is how many for-each iterations to perform.
                           // The second parameter is the batch size,
                           //   essentially the no-overhead inner-loop that just invokes Execute(i) in a loop.
                           //   When there is a lot of work in each iteration then a value of 1 can be sensible.
                           //   When there is very little work values of 32 or 64 can make sense.
                           // The third parameter is a JobHandle to use for this job's dependencies.
                           //   Dependencies are used to ensure that a job executes on worker threads after the dependency has completed execution.
                           JobHandle scheduleParallelJobHandle =
                               job.ScheduleParallel(m_enemies.Count, m_enemies.Count / 10, scheduleJobHandle);

                           break;
                       }
                   }

                   break;
               }
           }
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           MoveEnemyParallelJob job = new MoveEnemyParallelJob
           {
               deltaTime = deltaTime,
               positions = positions,
               moveYs = moveYs
           };

           // Schedule a parallel-for job. First parameter is how many for-each iterations to perform.
           // The second parameter is the batch size,
           // essentially the no-overhead inner-loop that just invokes Execute(i) in a loop.
           // When there is a lot of work in each iteration then a value of 1 can be sensible.
           // When there is very little work values of 32 or 64 can make sense.
           JobHandle jobHandle = job.Schedule(m_enemies.Count, m_enemies.Count / 10);

           }
   }

Recuperar los datos de un trabajo

Tenemos que esperar a que se complete el trabajo. Podemos obtener el estado del JobHandleque usamos cuando programamos el trabajo para completarlo. Esto esperará a que se complete el trabajo antes de continuar con la ejecución: > handle.Complete();o JobHandle.CompleteAll(jobHandles). Una vez que se completa el trabajo, el NativeContainerque usamos para configurar el trabajo tendrá todos los datos que necesitamos usar. Una vez que recuperamos los datos de ellos, tenemos que desecharlos adecuadamente.

El trabajo intensivo en rendimiento

Esto es bastante simple ya que no estoy leyendo ni escribiendo ningún dato en el trabajo. Espero a que se completen todos los trabajos que estaban programados y luego me deshago de la Nativematriz:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   ...

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Job:
               ....
               JobHandle.CompleteAll(jobHandles);
               jobHandles.Dispose();
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       ...
   }
}

El trabajo intensivo tarda alrededor de 6 ms en completarse y el juego se ejecuta a unos 90 FPS.

el MoveEnemytrabajo

Agrego los cheques completos apropiados:

   private void MoveEnemyJob(float deltaTime)
   {
      ....

       if (method == MethodType.MoveEnemyJob)
       {
          ....

           switch (moveEnemyJobType)
           {
               case MoveEnemyJobType.ScheduleSingleWorkerThread:
               case MoveEnemyJobType.ScheduleParallelWorkerThreads:
               {
                   ....

                   // typically one or the other
                   switch (moveEnemyJobType)
                   {
                       case MoveEnemyJobType.ScheduleSingleWorkerThread:
                           scheduleJobHandle.Complete();
                           break;
                       case MoveEnemyJobType.ScheduleParallelWorkerThreads:
                       {
                           scheduleParallelJobHandle.Complete();
                           break;
                       }
                   }

                   break;
               }
           }
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           ....

          jobHandle.Complete();
       }
   }

Después de las comprobaciones del tipo de método, recorro a todos los enemigos, establezco sus transformposiciones y moveYlos datos que se establecieron en el trabajo. A continuación, me deshago adecuadamente de las matrices nativas:

private void MoveEnemyJob(float deltaTime)
   {
      ....

       if (method == MethodType.MoveEnemyJob)
       {
          ....
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           ....
       }

       for (int i = 0; i < m_enemies.Count; i++)
       {
           m_enemies[i].transform.position = positions[i];
           m_enemies[i].moveY = moveYs[i];
       }

       // Native arrays must be disposed manually.
       positions.Dispose();
       moveYs.Dispose();
   }

Mostrar y mover mil enemigos con el trabajo tomó alrededor de 160 ms con una velocidad de cuadro de aproximadamente 7 FPS sin ganancias de rendimiento.

Mostrar y mover mil enemigos con el trabajo en paralelo tomó alrededor de 30 ms con una velocidad de cuadro de aproximadamente 30 FPS.

¿Qué es el compilador de ráfagas en Unity?

El compilador de ráfagas es un compilador que traduce de bytecode a código nativo. Usar esto con C# Job System mejora la calidad del código generado, lo que le brinda un aumento significativo en el rendimiento y reduce el consumo de batería en los dispositivos móviles.

Para usar esto, simplemente dígale a Unity que desea usar la compilación en ráfaga en el trabajo con el [BurstCompile]atributo:

using Unity.Burst;
using Unity.Jobs;
using Unity.Mathematics;

[BurstCompile]
public struct PerformanceIntensiveJob : IJob
{
   ...
}


using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;

[BurstCompile]
public struct MoveEnemyJob : IJobFor
{
   ...
}
[BurstCompile]
public struct MoveEnemyParallelJob : IJobParallelFor
{
   ...
}

Luego, en Unity, seleccione Trabajos > Ráfaga > Habilitar finalización

Burst es Just-In-Time (JIT) mientras está en el Editor, lo que significa que esto puede estar inactivo mientras está en Modo de reproducción. Cuando construyes tu proyecto es Ahead-Of-Time (AOT), lo que significa que esto debe habilitarse antes de construir tu proyecto. Puede hacerlo editando la sección Configuración de AOT de ráfaga en la ventana Configuración del proyecto .

Para obtener más detalles sobre el compilador de ráfagas, consulte la documentación de Unity .

Un trabajo de alto rendimiento con el compilador de ráfagas

Un trabajo intensivo con ráfaga tarda alrededor de 3 ms en completarse y el juego se ejecuta a unos 150 FPS.

Mostrando y moviendo mil enemigos, el trabajo con ráfaga tomó alrededor de 30 ms con una velocidad de cuadro de aproximadamente 30 FPS.

Mostrando y moviendo mil enemigos, el trabajo paralelo con la ráfaga tomó alrededor de 6 ms con una velocidad de cuadro de aproximadamente 80 a 90 FPS.

Conclusión

Podemos utilizar Taskpara aumentar el rendimiento de nuestras aplicaciones de Unity, pero existen varios inconvenientes al usarlos. Es mejor usar las cosas que vienen empaquetadas en Unity dependiendo de lo que queramos hacer. Usar corrutinas si queremos esperar a que algo termine de cargarse de forma asíncrona; podemos iniciar la rutina y no detener la ejecución del proceso de nuestro programa.

Podemos usar el sistema de trabajo de C# con el compilador de ráfagas para obtener una ganancia masiva en el rendimiento sin tener que preocuparnos por todas las cuestiones de administración de subprocesos cuando se realizan tareas de procesos intensivos. Usando los sistemas incorporados, estamos seguros de que se hace de una manera segura que no causa errores o errores no deseados.

Las tareas se ejecutaron un poco mejor que los trabajos sin usar el compilador de ráfagas, pero eso se debe a la pequeña sobrecarga adicional detrás de escena para configurar todo de manera segura para nosotros. Al usar el compilador de ráfagas, nuestros trabajos realizaron nuestras tareas. Cuando necesite todo el rendimiento adicional que puede obtener, use el sistema de trabajo de C# con ráfaga.

Los archivos del proyecto para esto se pueden encontrar en mi GitHub .

Fuente: https://blog.logrocket.com/performance-unity-async-await-tasks-coroutines-c-job-system-burst-compiler/ 

#csharp #async #await 

What is GEEK

Buddha Community

Qué son Async, Await y Task en C#

Qué son Async, Await y Task en C#

El rendimiento lo es todo cuando intenta publicar en la web, dispositivos móviles, consolas e incluso algunas de las PC de gama baja. Un juego o aplicación que se ejecuta a menos de 30 FPS puede causar frustración a los usuarios. Echemos un vistazo a algunas de las cosas que podemos usar para aumentar el rendimiento al reducir la carga en la CPU.

En esta publicación, cubriremos qué son async, awaity Tasken C# y cómo usarlos en Unity para obtener rendimiento en su proyecto. A continuación, veremos algunos de los paquetes integrados de Unity: rutinas, el sistema de trabajo de C# y el compilador de ráfagas. Veremos qué son, cómo usarlos y cómo aumentan el rendimiento en su proyecto.

Para comenzar este proyecto, usaré Unity 2021.3.4f1. No he probado este código en ninguna otra versión de Unity; todos los conceptos aquí deberían funcionar en cualquier versión de Unity posterior a Unity 2019.3. Sus resultados de rendimiento pueden diferir si usa una versión anterior, ya que Unity realizó algunas mejoras significativas con el modelo de programación async/await en 2021. Obtenga más información al respecto en el blog de Unity Unity and .NET, what's next , en particular, la sección denominada "Modernizing the Tiempo de ejecución de la unidad”.

Creé un nuevo proyecto Core 2D (URP), pero puede usarlo en cualquier tipo de proyecto que desee.

Tengo un sprite que obtuve de Space Shooter (Redux, además de fuentes y sonidos) de Kenney Vleugels .

Creé un prefabricado enemigo que contiene un Sprite Render y un Enemy Component. El Enemy Component es un MonoBehaviourque tiene a Transformy a floatpara realizar un seguimiento de la posición y la velocidad para moverse en el eje y:

using UnityEngine;

public class Enemy
{
   public Transform transform;
   public float moveY;
}

Qué async, awaity Taskson en C#

¿Qué es async?

En C#, los métodos pueden tener una asyncpalabra clave delante de ellos, lo que significa que los métodos son métodos asincrónicos. Esta es solo una forma de decirle al compilador que queremos poder ejecutar el código interno y permitir que la persona que llama a ese método continúe con la ejecución mientras espera que finalice este método.

Un ejemplo de esto sería cocinar una comida. Comenzarás a cocinar la carne, y mientras la carne se cocina y esperas a que termine, comenzarás a hacer los lados. Mientras se cocina la comida, empezarías a poner la mesa. Un ejemplo de esto en código sería static async Task<Steak> MakeSteak(int number).

Unity también tiene todo tipo de métodos incorporados a los que puede llamar de forma asíncrona; consulte los documentos de Unity para obtener una lista de métodos. Con la forma en que Unity maneja la administración de la memoria, utiliza corrutinas oAsyncOperation el sistema de trabajo de C# .

¿Qué es awaity cómo se usa?

En C#, puede esperar a que se complete una operación asíncrona usando la awaitpalabra clave. Esto se usa dentro de cualquier método que tenga la asyncpalabra clave para esperar a que continúe una operación:

Public async void Update()
{
     // do stuff
     await // some asynchronous method or task to finish
     // do more stuff or do stuff with the data returned from the asynchronous task.
}

Consulte los documentos de Microsoft para obtener más información sobre await.

¿Qué es un Tasky cómo se usa?

A Taskes un método asíncrono que realiza una única operación y no devuelve ningún valor. Para a Taskque devuelve un valor, usaríamos Task<TResult>.

Para usar una tarea, la creamos como crear cualquier objeto nuevo en C#: Task t1 = new Task(void Action). A continuación, comenzamos la tarea t1.wait. Por último, esperamos a que la tarea se complete con t1.wait.

Hay varias formas de crear, iniciar y ejecutar tareas. Task t2 = Task.Run(void Action)creará y comenzará una tarea. await Task.Run(void Action)creará, iniciará y esperará a que se complete la tarea. Podemos usar la forma alternativa más común con Task t3 = Task.Factory.Start(void Action).

Hay varias formas en que podemos esperar a que se complete la tarea. int index = Task.WaitAny(Task[])esperará a que se complete cualquier tarea y nos dará el índice de la tarea completada en la matriz. await Task.WaitAll(Task[])esperará a que se completen todas las tareas.

Para obtener más información sobre las tareas, consulte los documentos de Microsoft .

un ejemplo sencillotask

private void Start()
{
   Task t1 = new Task(() => Thread.Sleep(1000));
   Task t2 = Task.Run(() => Thread.Sleep(2000000));
   Task t3 = Task.Factory.StartNew(() => Thread.Sleep(1000));
   t1.Start();
   Task[] tasks = { t1, t2, t3 };
   int index = Task.WaitAny(tasks);
   Debug.Log($"Task {tasks[index].Id} at index {index} completed.");

   Task t4 = new Task(() => Thread.Sleep(100));
   Task t5 = Task.Run(() => Thread.Sleep(200));
   Task t6 = Task.Factory.StartNew(() => Thread.Sleep(300));
   t4.Start();
   Task.WaitAll(t4, t5, t6);
   Debug.Log($"All Task Completed!");
   Debug.Log($"Task When any t1={t1.IsCompleted} t2={t2.IsCompleted} t3={t3.IsCompleted}");
   Debug.Log($"All Task Completed! t4={t4.IsCompleted} t5={t5.IsCompleted} t6={t6.IsCompleted}");
}

public async void Update()
{
   float startTime = Time.realtimeSinceStartup;
   Debug.Log($"Update Started: {startTime}");
   Task t1 = new Task(() => Thread.Sleep(10000));
   Task t2 = Task.Run(() => Thread.Sleep(20000));
   Task t3 = Task.Factory.StartNew(() => Thread.Sleep(30000));

   await Task.WhenAll(t1, t2, t3);
   Debug.Log($"Update Finished: {(Time.realtimeSinceStartup - startTime) * 1000f} ms");
}

Cómo afecta la tarea al rendimiento

Ahora comparemos el rendimiento de una tarea con el rendimiento de un método.

Necesitaré una clase estática que pueda usar en todas mis comprobaciones de rendimiento. Tendrá un método y una tarea que simule una operación intensiva en rendimiento. Tanto el método como la tarea realizan exactamente la misma operación:

using System.Threading.Tasks;
using Unity.Mathematics;

public static class Performance
{
   public static void PerformanceIntensiveMethod(int timesToRepeat)
   {
       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < timesToRepeat; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }

   public static Task PerformanceIntensiveTask(int timesToRepeat)
   {
       return Task.Run(() =>
       {
           // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
           float value = 0f;
           for (int i = 0; i < timesToRepeat; i++)
           {
               value = math.exp10(math.sqrt(value));
           }
       });
   }
}

Ahora necesito uno MonoBehaviourque pueda usar para probar el impacto en el rendimiento de la tarea y el método. Solo para poder ver un mejor impacto en el rendimiento, fingiré que quiero ejecutar esto en diez objetos de juego diferentes. También realizaré un seguimiento de la cantidad de tiempo Updateque tarda en ejecutarse el método.

En Update, obtengo la hora de inicio. Si estoy probando el método, recorro todos los objetos del juego simulado y llamo al método intensivo en rendimiento. Si estoy probando la tarea, creo un nuevo Taskbucle de matriz a través de todos los objetos del juego simulado y agrego la tarea de rendimiento intensivo a la matriz de tareas. Entonces awaitpara todas las tareas para completar. Fuera de la verificación del tipo de método, actualizo el tiempo del método, convirtiéndolo a ms. También lo registro.

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task
   }

   [SerializeField] private int numberGameObjectsToImitate
= 10;

   [SerializeField] private MethodType method = MethodType.Normal;

   [SerializeField] private float methodTime;

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           case MethodType.Normal:
               for (int i = 0; i < numberGameObjectsToImitate
; i++)
                   Performance.PerformanceIntensiveMethod(50000);
               break;
           case MethodType.Task:
               Task[] tasks = new Task[numberGameObjectsToImitate
];
               for (int i = 0; i < numberGameObjectsToImitate
; i++)
                   tasks[i] = Performance.PerformanceIntensiveTask(5000);
               await Task.WhenAll(tasks);
               break;
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
   }
}

El método intensivo tarda alrededor de 65 ms en completarse y el juego se ejecuta a unos 12 FPS.

La tarea intensiva tarda alrededor de 4 ms en completarse y el juego se ejecuta a unos 200 FPS.

Intentemos esto con mil enemigos:

using System.Collections.Generic;
using System.Threading.Tasks;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
using Random = UnityEngine.Random;

public class PerformanceTaskJob : MonoBehaviour
{
   private enum MethodType
   {
       NormalMoveEnemy,
       TaskMoveEnemy
   }

   [SerializeField] private int numberEnemiesToCreate = 1000;
   [SerializeField] private Transform pfEnemy;

   [SerializeField] private MethodType method = MethodType.NormalMoveEnemy;
   [SerializeField] private float methodTime;

   private readonly List<Enemy> m_enemies = new List<Enemy>();

   private void Start()
   {
       for (int i = 0; i < numberEnemiesToCreate; i++)
       {
           Transform enemy = Instantiate(pfEnemy,
                                         new Vector3(Random.Range(-8f, 8f), Random.Range(-8f, 8f)),
                                         Quaternion.identity);
           m_enemies.Add(new Enemy { transform = enemy, moveY = Random.Range(1f, 2f) });
       }
   }

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           case MethodType.NormalMoveEnemy:
               MoveEnemy();
               break;
           case MethodType.TaskMoveEnemy:
               Task<Task[]> moveEnemyTasks = MoveEnemyTask();
               await Task.WhenAll(moveEnemyTasks);
               break;
           default:
               MoveEnemy();
               break;
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
   }

   private void MoveEnemy()
   {
       foreach (Enemy enemy in m_enemies)
       {
           enemy.transform.position += new Vector3(0, enemy.moveY * Time.deltaTime);
           if (enemy.transform.position.y > 5f)
               enemy.moveY = -math.abs(enemy.moveY);
           if (enemy.transform.position.y < -5f)
               enemy.moveY = +math.abs(enemy.moveY);
           Performance.PerformanceIntensiveMethod(1000);
       }
   }

   private async Task<Task[]> MoveEnemyTask()
   {
       Task[] tasks = new Task[m_enemies.Count];
       for (int i = 0; i < m_enemies.Count; i++)
       {
           Enemy enemy = m_enemies[i];
           enemy.transform.position += new Vector3(0, enemy.moveY * Time.deltaTime);
           if (enemy.transform.position.y > 5f)
               enemy.moveY = -math.abs(enemy.moveY);
           if (enemy.transform.position.y < -5f)
               enemy.moveY = +math.abs(enemy.moveY);
           tasks[i] = Performance.PerformanceIntensiveTask(1000);
       }

       await Task.WhenAll(tasks);

       return tasks;
  }

Mostrar y mover mil enemigos con el método tomó alrededor de 150 ms con una velocidad de cuadro de aproximadamente 7 FPS.

Mostrar y mover mil enemigos con una tarea tomó alrededor de 50 ms con una velocidad de cuadro de aproximadamente 30 FPS.

¿Por qué no useTasks?

Las tareas son extremadamente eficientes y reducen la tensión en el rendimiento de su sistema. Incluso puede usarlos en múltiples subprocesos usando la Biblioteca paralela de tareas (TPL).

Sin embargo, existen algunos inconvenientes al usarlos en Unity. El principal inconveniente de usar TaskUnity es que todos se ejecutan en el Mainsubproceso. Sí, podemos hacer que se ejecuten en otros subprocesos, pero Unity ya realiza su propia gestión de subprocesos y memoria, y puede crear errores al crear más subprocesos que núcleos de CPU, lo que genera competencia por los recursos.

Las tareas también pueden ser difíciles de realizar correctamente y depurar. Al escribir el código original, terminé con todas las tareas ejecutándose, pero ninguno de los enemigos se movió en la pantalla. Terminó siendo que necesitaba devolver el Task[]que creé en el archivo Task.

Las tareas crean mucha basura que afecta el rendimiento. Tampoco aparecen en el generador de perfiles, por lo que si tiene uno que afecta el rendimiento, es difícil rastrearlo. Además, he notado que a veces mis tareas y funciones de actualización continúan ejecutándose desde otras escenas.

Corrutinas de Unity

Según Unity , "una corrutina es una función que puede suspender su ejecución (rendimiento) hasta que finalice la instrucción de rendimiento dada " .

Lo que esto significa es que podemos ejecutar código y esperar a que se complete una tarea antes de continuar. Esto es muy parecido a un método asíncrono. Utiliza un tipo de retorno IEnumeratory we yield returnen lugar de await.

Unity tiene varios tipos diferentes de instrucciones de rendimiento que podemos usar, es decir, WaitForSeconds, WaitForEndOfFrame, WaitUntilo WaitWhile.

Para iniciar las corrutinas, necesitamos MonoBehavioury usamos el MonoBehaviour.StartCoroutine.

Para detener una rutina antes de que se complete, usamos MonoBehaviour.StopCoroutine. Al detener las rutinas, asegúrese de utilizar el mismo método que utilizó para iniciarlas.

Los casos de uso comunes para corrutinas en Unity son esperar a que se carguen los activos y crear temporizadores de enfriamiento.

Ejemplo: un cargador de escenas usando una corrutina

using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;

public class SceneLoader : MonoBehaviour
{
   public Coroutine loadSceneCoroutine;
   public void Update()
   {
       if (Input.GetKeyDown(KeyCode.Space) && loadSceneCoroutine == null)
       {
           loadSceneCoroutine = StartCoroutine(LoadSceneAsync());
       }

       if (Input.GetKeyDown(KeyCode.Escape) && loadSceneCoroutine != null)
       {
           StopCoroutine(loadSceneCoroutine);
           loadSceneCoroutine = null;
       }
   }

   private IEnumerator LoadSceneAsync()
   {
       AsyncOperation asyncLoad = SceneManager.LoadSceneAsync("Scene2");
       yield return new WaitWhile(() => !asyncLoad.isDone);
   }
}

Comprobar el impacto de una rutina en el rendimiento

Veamos cómo el uso de una corrutina afecta el rendimiento de nuestro proyecto. Solo voy a hacer esto con el método intensivo en rendimiento.

Agregué Coroutinea la MethodTypeenumeración y las variables para realizar un seguimiento de su estado:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task,
       Coroutine
   }

   ...

   private Coroutine m_performanceCoroutine;

Creé la rutina. Esto es similar a la tarea y el método de alto rendimiento que creamos anteriormente con código agregado para actualizar el tiempo del método:

   private IEnumerator PerformanceCoroutine(int timesToRepeat, float startTime)
   {
       for (int count = 0; count < numberGameObjectsToImitate; count++)
       {
           // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
           float value = 0f;
           for (int i = 0; i < timesToRepeat; i++)
           {
               value = math.exp10(math.sqrt(value));
           }
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
       m_performanceCoroutine = null;
       yield return null;
   }

En el Updatemétodo, agregué el cheque para la rutina. También modifiqué el tiempo del método, actualicé el código y agregué código para detener la rutina si se estaba ejecutando y cambiamos el tipo de método:

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Coroutine:
               m_performanceCoroutine ??= StartCoroutine(PerformanceCoroutine(5000, startTime));
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       if (method != MethodType.Coroutine)
       {
           methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
           Debug.Log($"{methodTime} ms");
       }

       if (method != MethodType.Coroutine || m_performanceCoroutine == null) return;
       StopCoroutine(m_performanceCoroutine);
       m_performanceCoroutine = null;
   }

La rutina intensiva tarda alrededor de 6 ms en completarse y el juego se ejecuta a unos 90 FPS.

El sistema de trabajos de C# y el compilador de ráfagas

¿Qué es el sistema de trabajo de C#?

El sistema de trabajo de C# es la implementación de Unity de tareas que son fáciles de escribir, no generan la basura que generan las tareas y utilizan los subprocesos de trabajo que Unity ya ha creado. Esto corrige todas las desventajas de las tareas.

Unity compara los trabajos como subprocesos, pero dicen que un trabajo realiza una tarea específica. Los trabajos también pueden depender de otros trabajos para completarse antes de ejecutarse; esto soluciona el problema con la tarea que tenía que no se movió correctamente Unitsporque dependía de que otra tarea se completara primero.

Unity se ocupa automáticamente de las dependencias laborales. El sistema de trabajo también tiene un sistema de seguridad incorporado principalmente para proteger contra las condiciones de carrera . Una advertencia con los trabajos es que solo pueden contener variables miembro que sean tipos blittables o tipos NativeContainer ; esto es un inconveniente del sistema de seguridad.

Para usar el sistema de trabajos, cree el trabajo, programe el trabajo, espere a que se complete y luego use los datos devueltos por el trabajo. El sistema de trabajo es necesario para usar la pila de tecnología orientada a datos (DOTS) de Unity.

Para obtener más detalles sobre el sistema de trabajo, consulte la documentación de Unity .

Creando un trabajo

Para crear un trabajo, cree uno stuctque implemente una de las IJobinterfaces ( IJob , IJobFor , IJobParallelFor , Unity.Engine.Jobs.IJobParallelForTransform ). IJobes un trabajo básico. IJobFory IJobForParallelse utilizan para realizar la misma operación en cada elemento de un contenedor nativo o para varias iteraciones. La diferencia entre ellos es que IJobFor se ejecuta en un solo subproceso donde IJobForParallelse dividirá entre varios subprocesos.

Lo usaré IJobpara crear un trabajo de operación intensiva IJobFory IJobForParallelpara crear un trabajo que moverá a múltiples enemigos; esto es solo para que podamos ver los diferentes impactos en el rendimiento. Estos trabajos serán idénticos a las tareas y métodos que creamos anteriormente:

public struct PerformanceIntensiveJob : IJob { }
public struct MoveEnemyJob: IJobFor { }
public struct MoveEnemyParallelJob : IJobParallelFor { }

Agregue las variables miembro. En mi caso, mi IJobno necesita ninguno. Los IJobFory IJobParallelFornecesitan un valor flotante para el tiempo delta actual, ya que los trabajos no tienen un concepto de marco; operan fuera de Unity MonoBehaviour. También necesitan una matriz float3para la posición y una matriz para la velocidad de movimiento en el eje y:

public struct MoveEnemyJob : IJobFor
{
   public NativeArray<float3> positions;
   public NativeArray<float> moveYs;
   public float deltaTime; 
}
public struct MoveEnemyParallelJob : IJobParallelFor
{
   public NativeArray<float3> positions;
   public NativeArray<float> moveYs;
   public float deltaTime;
}

El último paso es implementar el Executemétodo requerido. Los IJobFory IJobForParallelrequieren un intpara el índice de la iteración actual que se está ejecutando el trabajo.

La diferencia es que en lugar de acceder a los movimientos del enemigo transform, usamos la matriz que está en el trabajo:

public struct PerformanceIntensiveJob : IJob
{
   #region Implementation of IJob

   /// <inheritdoc />
   public void Execute()
   {
       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < 50000; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }

   #endregion
}

// MoveEnemyJob and MoveEnemyParallelJob have the same exact Execute Method. 

   /// <inheritdoc />
   public void Execute(int index)
   {
       positions[index] += new float3(0, moveYs[index] * deltaTime, 0);
       if (positions[index].y > 5f)
           moveYs[index] = -math.abs(moveYs[index]);
       if (positions[index].y < -5f)
           moveYs[index] = +math.abs(moveYs[index]);

       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < 1000; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }   private JobHandle PerformanceIntensiveMethodJob()
   {
       PerformanceIntensiveJob job = new PerformanceIntensiveJob();
       return job.Schedule();
   }

Programar un trabajo

Primero, necesitamos instalar el trabajo y completar los datos del trabajo:

NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);

MyJob jobData = new MyJob();
jobData.myFloat = result;

Luego programamos el trabajo con JobHandle jobHandle = jobData.Schedule();. El Schedulemétodo devuelve un JobHandleque se puede utilizar más adelante.

No podemos programar un trabajo desde dentro de un trabajo. Sin embargo, podemos crear nuevos trabajos y completar sus datos desde dentro de un trabajo. Una vez que se ha programado un trabajo, no se puede interrumpir.

El trabajo intensivo en rendimiento

Creé un método que crea un nuevo trabajo y lo programa. Devuelve el identificador de trabajo que puedo usar en mi updatemétodo:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   ....

   private JobHandle PerformanceIntensiveMethodJob()
   {
       PerformanceIntensiveJob job = new PerformanceIntensiveJob();
       return job.Schedule();
   }
}

Agregué el trabajo a mi enumeración. Luego, en el Updatemétodo, agrego el casea la switchsección. Creé una matriz de JobHandles. Luego recorro todos los objetos del juego simulado y agrego un trabajo programado para cada uno a la matriz:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task,
       Coroutine,
       Job
   }

   ...
   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Job:
               NativeArray<JobHandle> jobHandles =
                   new NativeArray<JobHandle>(numberGameObjectsToImitate, Allocator.Temp);
               for (int i = 0; i < numberGameObjectsToImitate; i++)
                   jobHandles[i] = PerformanceIntensiveMethodJob();
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       ...
   }
}

el MoveEnemyyMoveEnemyParallelJob

A continuación, agregué los trabajos a mi enumeración. Luego, en el Updatemétodo, llamo a un nuevo MoveEnemyJobmétodo, pasando el tiempo delta. Normalmente usaría el JobForo el JobParallelFor:

public class PerformanceTaskJob : MonoBehaviour
{
   private enum MethodType
   {
       NormalMoveEnemy,
       TaskMoveEnemy,
       MoveEnemyJob,
       MoveEnemyParallelJob
   }

   ...

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.MoveEnemyJob:
           case MethodType.MoveEnemyParallelJob:
               MoveEnemyJob(Time.deltaTime);
               break;
           default:
               MoveEnemy();
               break;
       }

       ...
   }

   ...

Lo primero que hago es establecer una matriz para las posiciones y una matriz para las moveYque pasaré a los trabajos. Luego lleno estas matrices con los datos de los enemigos. A continuación, creo el trabajo y configuro los datos del trabajo según el trabajo que quiero usar. Después de eso, programo el trabajo según el trabajo que quiero usar y el tipo de programación que quiero hacer:

private void MoveEnemyJob(float deltaTime)
   {
       NativeArray<float3> positions = new NativeArray<float3>(m_enemies.Count, Allocator.TempJob);
       NativeArray<float> moveYs = new NativeArray<float>(m_enemies.Count, Allocator.TempJob);

       for (int i = 0; i < m_enemies.Count; i++)
       {
           positions[i] = m_enemies[i].transform.position;
           moveYs[i] = m_enemies[i].moveY;
       }

       // Use one or the other
       if (method == MethodType.MoveEnemyJob)
       {
           MoveEnemyJob job = new MoveEnemyJob
           {
               deltaTime = deltaTime,
               positions = positions,
               moveYs = moveYs
           };

           // typically we would use one of these methods
           switch (moveEnemyJobType)
           {
               case MoveEnemyJobType.ImmediateMainThread:
                   // Schedule job to run immediately on main thread.
                   // typically would not use.
                   job.Run(m_enemies.Count);
                   break;
               case MoveEnemyJobType.ScheduleSingleWorkerThread:
               case MoveEnemyJobType.ScheduleParallelWorkerThreads:
               {
                   // Schedule job to run at a later point on a single worker thread.
                   // First parameter is how many for-each iterations to perform.
                   // The second parameter is a JobHandle to use for this job's dependencies.
                   //   Dependencies are used to ensure that a job executes on worker threads after the dependency has completed execution.
                   //   In this case we don't need our job to depend on anything so we can use a default one.
                   JobHandle scheduleJobDependency = new JobHandle();
                   JobHandle scheduleJobHandle = job.Schedule(m_enemies.Count, scheduleJobDependency);

                   switch (moveEnemyJobType)
                   {
                       case MoveEnemyJobType.ScheduleSingleWorkerThread:
                           break;
                       case MoveEnemyJobType.ScheduleParallelWorkerThreads:
                       {
                           // Schedule job to run on parallel worker threads.
                           // First parameter is how many for-each iterations to perform.
                           // The second parameter is the batch size,
                           //   essentially the no-overhead inner-loop that just invokes Execute(i) in a loop.
                           //   When there is a lot of work in each iteration then a value of 1 can be sensible.
                           //   When there is very little work values of 32 or 64 can make sense.
                           // The third parameter is a JobHandle to use for this job's dependencies.
                           //   Dependencies are used to ensure that a job executes on worker threads after the dependency has completed execution.
                           JobHandle scheduleParallelJobHandle =
                               job.ScheduleParallel(m_enemies.Count, m_enemies.Count / 10, scheduleJobHandle);

                           break;
                       }
                   }

                   break;
               }
           }
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           MoveEnemyParallelJob job = new MoveEnemyParallelJob
           {
               deltaTime = deltaTime,
               positions = positions,
               moveYs = moveYs
           };

           // Schedule a parallel-for job. First parameter is how many for-each iterations to perform.
           // The second parameter is the batch size,
           // essentially the no-overhead inner-loop that just invokes Execute(i) in a loop.
           // When there is a lot of work in each iteration then a value of 1 can be sensible.
           // When there is very little work values of 32 or 64 can make sense.
           JobHandle jobHandle = job.Schedule(m_enemies.Count, m_enemies.Count / 10);

           }
   }

Recuperar los datos de un trabajo

Tenemos que esperar a que se complete el trabajo. Podemos obtener el estado del JobHandleque usamos cuando programamos el trabajo para completarlo. Esto esperará a que se complete el trabajo antes de continuar con la ejecución: > handle.Complete();o JobHandle.CompleteAll(jobHandles). Una vez que se completa el trabajo, el NativeContainerque usamos para configurar el trabajo tendrá todos los datos que necesitamos usar. Una vez que recuperamos los datos de ellos, tenemos que desecharlos adecuadamente.

El trabajo intensivo en rendimiento

Esto es bastante simple ya que no estoy leyendo ni escribiendo ningún dato en el trabajo. Espero a que se completen todos los trabajos que estaban programados y luego me deshago de la Nativematriz:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   ...

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Job:
               ....
               JobHandle.CompleteAll(jobHandles);
               jobHandles.Dispose();
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       ...
   }
}

El trabajo intensivo tarda alrededor de 6 ms en completarse y el juego se ejecuta a unos 90 FPS.

el MoveEnemytrabajo

Agrego los cheques completos apropiados:

   private void MoveEnemyJob(float deltaTime)
   {
      ....

       if (method == MethodType.MoveEnemyJob)
       {
          ....

           switch (moveEnemyJobType)
           {
               case MoveEnemyJobType.ScheduleSingleWorkerThread:
               case MoveEnemyJobType.ScheduleParallelWorkerThreads:
               {
                   ....

                   // typically one or the other
                   switch (moveEnemyJobType)
                   {
                       case MoveEnemyJobType.ScheduleSingleWorkerThread:
                           scheduleJobHandle.Complete();
                           break;
                       case MoveEnemyJobType.ScheduleParallelWorkerThreads:
                       {
                           scheduleParallelJobHandle.Complete();
                           break;
                       }
                   }

                   break;
               }
           }
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           ....

          jobHandle.Complete();
       }
   }

Después de las comprobaciones del tipo de método, recorro a todos los enemigos, establezco sus transformposiciones y moveYlos datos que se establecieron en el trabajo. A continuación, me deshago adecuadamente de las matrices nativas:

private void MoveEnemyJob(float deltaTime)
   {
      ....

       if (method == MethodType.MoveEnemyJob)
       {
          ....
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           ....
       }

       for (int i = 0; i < m_enemies.Count; i++)
       {
           m_enemies[i].transform.position = positions[i];
           m_enemies[i].moveY = moveYs[i];
       }

       // Native arrays must be disposed manually.
       positions.Dispose();
       moveYs.Dispose();
   }

Mostrar y mover mil enemigos con el trabajo tomó alrededor de 160 ms con una velocidad de cuadro de aproximadamente 7 FPS sin ganancias de rendimiento.

Mostrar y mover mil enemigos con el trabajo en paralelo tomó alrededor de 30 ms con una velocidad de cuadro de aproximadamente 30 FPS.

¿Qué es el compilador de ráfagas en Unity?

El compilador de ráfagas es un compilador que traduce de bytecode a código nativo. Usar esto con C# Job System mejora la calidad del código generado, lo que le brinda un aumento significativo en el rendimiento y reduce el consumo de batería en los dispositivos móviles.

Para usar esto, simplemente dígale a Unity que desea usar la compilación en ráfaga en el trabajo con el [BurstCompile]atributo:

using Unity.Burst;
using Unity.Jobs;
using Unity.Mathematics;

[BurstCompile]
public struct PerformanceIntensiveJob : IJob
{
   ...
}


using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;

[BurstCompile]
public struct MoveEnemyJob : IJobFor
{
   ...
}
[BurstCompile]
public struct MoveEnemyParallelJob : IJobParallelFor
{
   ...
}

Luego, en Unity, seleccione Trabajos > Ráfaga > Habilitar finalización

Burst es Just-In-Time (JIT) mientras está en el Editor, lo que significa que esto puede estar inactivo mientras está en Modo de reproducción. Cuando construyes tu proyecto es Ahead-Of-Time (AOT), lo que significa que esto debe habilitarse antes de construir tu proyecto. Puede hacerlo editando la sección Configuración de AOT de ráfaga en la ventana Configuración del proyecto .

Para obtener más detalles sobre el compilador de ráfagas, consulte la documentación de Unity .

Un trabajo de alto rendimiento con el compilador de ráfagas

Un trabajo intensivo con ráfaga tarda alrededor de 3 ms en completarse y el juego se ejecuta a unos 150 FPS.

Mostrando y moviendo mil enemigos, el trabajo con ráfaga tomó alrededor de 30 ms con una velocidad de cuadro de aproximadamente 30 FPS.

Mostrando y moviendo mil enemigos, el trabajo paralelo con la ráfaga tomó alrededor de 6 ms con una velocidad de cuadro de aproximadamente 80 a 90 FPS.

Conclusión

Podemos utilizar Taskpara aumentar el rendimiento de nuestras aplicaciones de Unity, pero existen varios inconvenientes al usarlos. Es mejor usar las cosas que vienen empaquetadas en Unity dependiendo de lo que queramos hacer. Usar corrutinas si queremos esperar a que algo termine de cargarse de forma asíncrona; podemos iniciar la rutina y no detener la ejecución del proceso de nuestro programa.

Podemos usar el sistema de trabajo de C# con el compilador de ráfagas para obtener una ganancia masiva en el rendimiento sin tener que preocuparnos por todas las cuestiones de administración de subprocesos cuando se realizan tareas de procesos intensivos. Usando los sistemas incorporados, estamos seguros de que se hace de una manera segura que no causa errores o errores no deseados.

Las tareas se ejecutaron un poco mejor que los trabajos sin usar el compilador de ráfagas, pero eso se debe a la pequeña sobrecarga adicional detrás de escena para configurar todo de manera segura para nosotros. Al usar el compilador de ráfagas, nuestros trabajos realizaron nuestras tareas. Cuando necesite todo el rendimiento adicional que puede obtener, use el sistema de trabajo de C# con ráfaga.

Los archivos del proyecto para esto se pueden encontrar en mi GitHub .

Fuente: https://blog.logrocket.com/performance-unity-async-await-tasks-coroutines-c-job-system-burst-compiler/ 

#csharp #async #await 

Tamale  Moses

Tamale Moses

1624240146

How to Run C/C++ in Sublime Text?

C and C++ are the most powerful programming language in the world. Most of the super fast and complex libraries and algorithms are written in C or C++. Most powerful Kernel programs are also written in C. So, there is no way to skip it.

In programming competitions, most programmers prefer to write code in C or C++. Tourist is considered the worlds top programming contestant of all ages who write code in C++.

During programming competitions, programmers prefer to use a lightweight editor to focus on coding and algorithm designing. VimSublime Text, and Notepad++ are the most common editors for us. Apart from the competition, many software developers and professionals love to use Sublime Text just because of its flexibility.

I have discussed the steps we need to complete in this blog post before running a C/C++ code in Sublime Text. We will take the inputs from an input file and print outputs to an output file without using freopen file related functions in C/C++.

#cpp #c #c-programming #sublimetext #c++ #c/c++

Dicey Issues in C/C++

If you are familiar with C/C++then you must have come across some unusual things and if you haven’t, then you are about to. The below codes are checked twice before adding, so feel free to share this article with your friends. The following displays some of the issues:

  1. Using multiple variables in the print function
  2. Comparing Signed integer with unsigned integer
  3. Putting a semicolon at the end of the loop statement
  4. C preprocessor doesn’t need a semicolon
  5. Size of the string matters
  6. Macros and equations aren’t good friends
  7. Never compare Floating data type with double data type
  8. Arrays have a boundary
  9. Character constants are different from string literals
  10. Difference between single(=) and double(==) equal signs.

The below code generates no error since a print function can take any number of inputs but creates a mismatch with the variables. The print function is used to display characters, strings, integers, float, octal, and hexadecimal values onto the output screen. The format specifier is used to display the value of a variable.

  1. %d indicates Integer Format Specifier
  2. %f indicates Float Format Specifier
  3. %c indicates Character Format Specifier
  4. %s indicates String Format Specifier
  5. %u indicates Unsigned Integer Format Specifier
  6. %ld indicates Long Int Format Specifier

Image for post


A signed integer is a 32-bit datum that encodes an integer in the range [-2147483648 to 2147483647]. An unsigned integer is a 32-bit datum that encodes a non-negative integer in the range [0 to 4294967295]. The signed integer is represented in twos-complement notation. In the below code the signed integer will be converted to the maximum unsigned integer then compared with the unsigned integer.

Image for post

#problems-with-c #dicey-issues-in-c #c-programming #c++ #c #cplusplus

Clara  Windler

Clara Windler

1625098080

Intro to Async/Await in C# .NET | Best Practices | C# Advance Concepts

This video contain full in-depth knowledge of async and await keyword in c# how you can do asynchronous programming in c# .NET. I will cover some common mistakes that you should avoid and some best practices that you should always follow. Watch till end.

Source Code – https://github.com/NoumanBaloch/Async_Await_C_Sharp_Demo

Playlist
C# Advance Concepts || SharpScripter
https://youtube.com/playlist?list=PLB0Ey9uUNK6hEw_Z-n3V8PjFG5DRzn09W

Visit SharpScripter to read the latest articles.
https://www.sharpscripter.com

Foundations of JavaScript Complete Course
https://www.udemy.com/foundations-of-javascript

Connect with Me on
Facebook Page
https://www.facebook.com/sharpscripter

Instagram
https://www.instagram.com/sharpscripter

Twitter
https://twitter.com/sharpscripter

My public code is here
Github – https://github.com/NoumanBaloch

#sharpscripter #async #await #csharp

#c# #net #csharp #await #async #github

O que Async, Await e Task estão em C #

Desempenho é tudo quando você está tentando publicar na web, dispositivos móveis, consoles e até mesmo alguns dos PCs de baixo custo. Um jogo ou aplicativo rodando a menos de 30 FPS pode causar frustração para os usuários. Vamos dar uma olhada em algumas das coisas que podemos usar para aumentar o desempenho reduzindo a carga na CPU.

Neste post, abordaremos o que são async, await, e Taskem C# e como usá-los no Unity para obter desempenho em seu projeto. Em seguida, vamos dar uma olhada em alguns dos pacotes embutidos do Unity: corrotinas, o C# Job System e o compilador de intermitência. Veremos o que são, como usá-los e como eles aumentam o desempenho em seu projeto.

Para iniciar este projeto, usarei o Unity 2021.3.4f1. Não testei este código em nenhuma outra versão do Unity; todos os conceitos aqui devem funcionar em qualquer versão do Unity após o Unity 2019.3. Seus resultados de desempenho podem diferir se você usar uma versão mais antiga, pois o Unity fez algumas melhorias significativas com o modelo de programação async/await em 2021. Leia mais sobre isso no blog do Unity Unity and .NET, o que vem a seguir , em particular a seção intitulada “Modernizing the Tempo de execução da unidade.”

Criei um novo projeto 2D (URP) Core, mas você pode usá-lo em qualquer tipo de projeto que desejar.

Eu tenho um sprite que peguei do Space Shooter (Redux, além de fontes e sons) de Kenney Vleugels .

Eu criei um prefab inimigo que contém um Sprite Render e um Enemy Component. O Componente Inimigo é um MonoBehaviourque tem a Transforme a floatpara acompanhar a posição e a velocidade para se mover no eixo y:

using UnityEngine;

public class Enemy
{
   public Transform transform;
   public float moveY;
}

O que async, awaite Taskestão em C#

O que é async?

Em C#, os métodos podem ter uma palavra- asyncchave na frente deles, o que significa que os métodos são métodos assíncronos. Esta é apenas uma maneira de dizer ao compilador que queremos poder executar o código e permitir que o chamador desse método continue a execução enquanto aguarda a conclusão desse método.

Um exemplo disso seria cozinhar uma refeição. Você começará a cozinhar a carne e, enquanto a carne estiver cozinhando e você estiver esperando que ela termine, você começará a fazer os lados. Enquanto a comida está cozinhando, você deve começar a colocar a mesa. Um exemplo disso no código seria static async Task<Steak> MakeSteak(int number).

O Unity também tem todos os tipos de métodos embutidos que você pode chamar de forma assíncrona; consulte os documentos do Unity para obter uma lista de métodos. Com a maneira como o Unity lida com o gerenciamento de memória, ele usa coroutines , AsyncOperation, ou o C# Job System .

O que é awaite como você usa?

Em C#, você pode aguardar a conclusão de uma operação assíncrona usando a palavra- awaitchave. Isso é usado dentro de qualquer método que tenha a asyncpalavra-chave para aguardar a continuação de uma operação:

Public async void Update()
{
     // do stuff
     await // some asynchronous method or task to finish
     // do more stuff or do stuff with the data returned from the asynchronous task.
}

Consulte os documentos da Microsoft para saber mais sobre await.

O que é um Taske como você o usa?

A Taské um método assíncrono que executa uma única operação e não retorna um valor. Para um Taskque retorna um valor, usaríamos Task<TResult>.

Para usar uma tarefa, nós a criamos como criar qualquer novo objeto em C#: Task t1 = new Task(void Action). Em seguida, iniciamos a tarefa t1.wait. Por fim, esperamos que a tarefa seja concluída com t1.wait.

Há várias maneiras de criar, iniciar e executar tarefas. Task t2 = Task.Run(void Action)irá criar e iniciar uma tarefa. await Task.Run(void Action)irá criar, iniciar e aguardar a conclusão da tarefa. Podemos usar a maneira alternativa mais comum com Task t3 = Task.Factory.Start(void Action).

Existem várias maneiras pelas quais podemos esperar que a tarefa seja concluída. int index = Task.WaitAny(Task[])aguardará a conclusão de qualquer tarefa e nos fornecerá o índice da tarefa concluída na matriz. await Task.WaitAll(Task[])aguardará a conclusão de todas as tarefas.

Para obter mais informações sobre tarefas, consulte os Documentos da Microsoft .

Um exemplo simplestask

private void Start()
{
   Task t1 = new Task(() => Thread.Sleep(1000));
   Task t2 = Task.Run(() => Thread.Sleep(2000000));
   Task t3 = Task.Factory.StartNew(() => Thread.Sleep(1000));
   t1.Start();
   Task[] tasks = { t1, t2, t3 };
   int index = Task.WaitAny(tasks);
   Debug.Log($"Task {tasks[index].Id} at index {index} completed.");

   Task t4 = new Task(() => Thread.Sleep(100));
   Task t5 = Task.Run(() => Thread.Sleep(200));
   Task t6 = Task.Factory.StartNew(() => Thread.Sleep(300));
   t4.Start();
   Task.WaitAll(t4, t5, t6);
   Debug.Log($"All Task Completed!");
   Debug.Log($"Task When any t1={t1.IsCompleted} t2={t2.IsCompleted} t3={t3.IsCompleted}");
   Debug.Log($"All Task Completed! t4={t4.IsCompleted} t5={t5.IsCompleted} t6={t6.IsCompleted}");
}

public async void Update()
{
   float startTime = Time.realtimeSinceStartup;
   Debug.Log($"Update Started: {startTime}");
   Task t1 = new Task(() => Thread.Sleep(10000));
   Task t2 = Task.Run(() => Thread.Sleep(20000));
   Task t3 = Task.Factory.StartNew(() => Thread.Sleep(30000));

   await Task.WhenAll(t1, t2, t3);
   Debug.Log($"Update Finished: {(Time.realtimeSinceStartup - startTime) * 1000f} ms");
}

Como a tarefa afeta o desempenho

Agora vamos comparar o desempenho de uma tarefa versus o desempenho de um método.

Vou precisar de uma classe estática que possa usar em todas as minhas verificações de desempenho. Ele terá um método e uma tarefa que simula uma operação de alto desempenho. Tanto o método quanto a tarefa executam a mesma operação exata:

using System.Threading.Tasks;
using Unity.Mathematics;

public static class Performance
{
   public static void PerformanceIntensiveMethod(int timesToRepeat)
   {
       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < timesToRepeat; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }

   public static Task PerformanceIntensiveTask(int timesToRepeat)
   {
       return Task.Run(() =>
       {
           // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
           float value = 0f;
           for (int i = 0; i < timesToRepeat; i++)
           {
               value = math.exp10(math.sqrt(value));
           }
       });
   }
}

Agora eu preciso de um MonoBehaviourque eu possa usar para testar o impacto do desempenho na tarefa e no método. Só para que eu possa ver um melhor impacto no desempenho, vou fingir que quero rodar isso em dez objetos de jogo diferentes. Também acompanharei a quantidade de tempo que o Updatemétodo leva para ser executado.

Em Update, recebo a hora de início. Se estou testando o método, percorro todos os objetos de jogo simulados e chamo o método de alto desempenho. Se estou testando a tarefa, crio um novo Taskloop de matriz por meio de todos os objetos de jogo simulados e adiciono a tarefa de alto desempenho à matriz de tarefas. Eu, então, awaitpara que todas as tarefas sejam concluídas. Fora da verificação do tipo de método, atualizo o tempo do método, convertendo-o para ms. Eu também registro.

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task
   }

   [SerializeField] private int numberGameObjectsToImitate
= 10;

   [SerializeField] private MethodType method = MethodType.Normal;

   [SerializeField] private float methodTime;

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           case MethodType.Normal:
               for (int i = 0; i < numberGameObjectsToImitate
; i++)
                   Performance.PerformanceIntensiveMethod(50000);
               break;
           case MethodType.Task:
               Task[] tasks = new Task[numberGameObjectsToImitate
];
               for (int i = 0; i < numberGameObjectsToImitate
; i++)
                   tasks[i] = Performance.PerformanceIntensiveTask(5000);
               await Task.WhenAll(tasks);
               break;
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
   }
}

O método intensivo leva cerca de 65ms para ser concluído com o jogo rodando a cerca de 12 FPS.

Método Intensivo

A tarefa intensiva leva cerca de 4ms para ser concluída com o jogo rodando a cerca de 200 FPS.

Tarefa Intensiva

Vamos tentar isso com mil inimigos:

using System.Collections.Generic;
using System.Threading.Tasks;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
using Random = UnityEngine.Random;

public class PerformanceTaskJob : MonoBehaviour
{
   private enum MethodType
   {
       NormalMoveEnemy,
       TaskMoveEnemy
   }

   [SerializeField] private int numberEnemiesToCreate = 1000;
   [SerializeField] private Transform pfEnemy;

   [SerializeField] private MethodType method = MethodType.NormalMoveEnemy;
   [SerializeField] private float methodTime;

   private readonly List<Enemy> m_enemies = new List<Enemy>();

   private void Start()
   {
       for (int i = 0; i < numberEnemiesToCreate; i++)
       {
           Transform enemy = Instantiate(pfEnemy,
                                         new Vector3(Random.Range(-8f, 8f), Random.Range(-8f, 8f)),
                                         Quaternion.identity);
           m_enemies.Add(new Enemy { transform = enemy, moveY = Random.Range(1f, 2f) });
       }
   }

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           case MethodType.NormalMoveEnemy:
               MoveEnemy();
               break;
           case MethodType.TaskMoveEnemy:
               Task<Task[]> moveEnemyTasks = MoveEnemyTask();
               await Task.WhenAll(moveEnemyTasks);
               break;
           default:
               MoveEnemy();
               break;
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
   }

   private void MoveEnemy()
   {
       foreach (Enemy enemy in m_enemies)
       {
           enemy.transform.position += new Vector3(0, enemy.moveY * Time.deltaTime);
           if (enemy.transform.position.y > 5f)
               enemy.moveY = -math.abs(enemy.moveY);
           if (enemy.transform.position.y < -5f)
               enemy.moveY = +math.abs(enemy.moveY);
           Performance.PerformanceIntensiveMethod(1000);
       }
   }

   private async Task<Task[]> MoveEnemyTask()
   {
       Task[] tasks = new Task[m_enemies.Count];
       for (int i = 0; i < m_enemies.Count; i++)
       {
           Enemy enemy = m_enemies[i];
           enemy.transform.position += new Vector3(0, enemy.moveY * Time.deltaTime);
           if (enemy.transform.position.y > 5f)
               enemy.moveY = -math.abs(enemy.moveY);
           if (enemy.transform.position.y < -5f)
               enemy.moveY = +math.abs(enemy.moveY);
           tasks[i] = Performance.PerformanceIntensiveTask(1000);
       }

       await Task.WhenAll(tasks);

       return tasks;
  }

Exibir e mover mil inimigos com o método levou cerca de 150ms com uma taxa de quadros de cerca de 7 FPS.

Mil Inimigos

Exibir e mover mil inimigos com uma tarefa levou cerca de 50ms com uma taxa de quadros de cerca de 30 FPS.

Exibindo Inimigos em Movimento

Por que não useTasks?

As tarefas são extremamente eficientes e reduzem a pressão sobre o desempenho do seu sistema. Você pode até usá-los em vários threads usando a Biblioteca Paralela de Tarefas (TPL).

No entanto, existem algumas desvantagens em usá-los no Unity. A principal desvantagem de usar Taskno Unity é que todos eles são executados no Mainencadeamento. Sim, podemos fazê-los rodar em outros threads, mas o Unity já faz seu próprio gerenciamento de thread e memória, e você pode criar erros criando mais threads do que CPU Cores, o que causa competição por recursos.

As tarefas também podem ser difíceis de executar corretamente e depurar. Ao escrever o código original, acabei com as tarefas todas em execução, mas nenhum dos inimigos se moveu na tela. Acabou sendo que precisei retornar o Task[]que criei no Task.

As tarefas criam muito lixo que afeta o desempenho. Eles também não aparecem no criador de perfil, portanto, se você tiver um que esteja afetando o desempenho, é difícil rastrear. Além disso, notei que às vezes minhas tarefas e funções de atualização continuam sendo executadas em outras cenas.

Corrotinas de unidade

De acordo com Unity , “Uma corrotina é uma função que pode suspender sua execução (yield) até que o YieldInstruction termine”.

O que isso significa é que podemos executar o código e esperar que uma tarefa seja concluída antes de continuar. Isso é muito parecido com um método assíncrono. Ele usa um tipo de retorno IEnumeratore nós yield returnem vez de await.

O Unity tem vários tipos diferentes de instruções de rendimento que podemos usar, ou seja, WaitForSeconds, WaitForEndOfFrame, WaitUntilou WaitWhile.

Para iniciar as corrotinas, precisamos de um MonoBehavioure usamos o MonoBehaviour.StartCoroutine.

Para parar uma corrotina antes que ela seja concluída, usamos MonoBehaviour.StopCoroutine. Ao parar as corrotinas, certifique-se de usar o mesmo método usado para iniciá-las.

Casos de uso comuns para corrotinas no Unity são aguardar o carregamento dos ativos e criar temporizadores de resfriamento.

Exemplo: Um carregador de cena usando uma corrotina

using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;

public class SceneLoader : MonoBehaviour
{
   public Coroutine loadSceneCoroutine;
   public void Update()
   {
       if (Input.GetKeyDown(KeyCode.Space) && loadSceneCoroutine == null)
       {
           loadSceneCoroutine = StartCoroutine(LoadSceneAsync());
       }

       if (Input.GetKeyDown(KeyCode.Escape) && loadSceneCoroutine != null)
       {
           StopCoroutine(loadSceneCoroutine);
           loadSceneCoroutine = null;
       }
   }

   private IEnumerator LoadSceneAsync()
   {
       AsyncOperation asyncLoad = SceneManager.LoadSceneAsync("Scene2");
       yield return new WaitWhile(() => !asyncLoad.isDone);
   }
}

Verificando o impacto de uma corrotina no desempenho

Vamos ver como o uso de uma corrotina afeta o desempenho do nosso projeto. Eu só vou fazer isso com o método de desempenho intensivo.

Eu adicionei Coroutineao MethodTypeenum e variáveis ​​para acompanhar seu estado:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task,
       Coroutine
   }

   ...

   private Coroutine m_performanceCoroutine;

Eu criei a corrotina. Isso é semelhante à tarefa e ao método de alto desempenho que criamos anteriormente com código adicionado para atualizar o tempo do método:

   private IEnumerator PerformanceCoroutine(int timesToRepeat, float startTime)
   {
       for (int count = 0; count < numberGameObjectsToImitate; count++)
       {
           // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
           float value = 0f;
           for (int i = 0; i < timesToRepeat; i++)
           {
               value = math.exp10(math.sqrt(value));
           }
       }

       methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
       Debug.Log($"{methodTime} ms");
       m_performanceCoroutine = null;
       yield return null;
   }

No Updatemétodo, adicionei a verificação da corrotina. Também modifiquei o tempo do método, atualizei o código e adicionei código para parar a corrotina se estivesse em execução e alteramos o tipo do método:

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Coroutine:
               m_performanceCoroutine ??= StartCoroutine(PerformanceCoroutine(5000, startTime));
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       if (method != MethodType.Coroutine)
       {
           methodTime = (Time.realtimeSinceStartup - startTime) * 1000f;
           Debug.Log($"{methodTime} ms");
       }

       if (method != MethodType.Coroutine || m_performanceCoroutine == null) return;
       StopCoroutine(m_performanceCoroutine);
       m_performanceCoroutine = null;
   }

A corrotina intensiva leva cerca de 6ms para ser concluída com o jogo rodando a cerca de 90 FPS.

Corrotina Intensiva

O sistema de trabalho C# e o compilador de intermitência

O que é o sistema de trabalho C#?

O C# Job System é a implementação do Unity de tarefas que são fáceis de escrever, não geram o lixo que as tarefas fazem e utilizam os threads de trabalho que o Unity já criou. Isso corrige todas as desvantagens das tarefas.

O Unity compara jobs como threads, mas eles dizem que um job faz uma tarefa específica. Os trabalhos também podem depender de outros trabalhos para serem concluídos antes de serem executados; isso corrige o problema com a tarefa que eu tinha que não moveu minha corretamente Unitsporque dependia de outra tarefa para ser concluída primeiro.

As dependências de trabalho são automaticamente cuidadas para nós pelo Unity. O sistema de trabalho também possui um sistema de segurança integrado principalmente para proteção contra condições de corrida . Uma ressalva com os jobs é que eles só podem conter variáveis ​​de membro que sejam tipos blittable ou tipos NativeContainer ; esta é uma desvantagem do sistema de segurança.

Para usar o sistema de trabalho, você cria o trabalho, agende o trabalho, aguarde a conclusão do trabalho e use os dados retornados pelo trabalho. O sistema de trabalho é necessário para usar o Data-Oriented Technology Stack (DOTS) da Unity.

Para obter mais detalhes sobre o sistema de tarefas, consulte a documentação do Unity .

Criando um trabalho

Para criar um trabalho, você cria um stuctque implementa uma das IJobinterfaces ( IJob , IJobFor , IJobParallelFor , Unity.Engine.Jobs.IJobParallelForTransform ). IJobé um trabalho básico. IJobFore IJobForParallelsão usados ​​para executar a mesma operação em cada elemento de um contêiner nativo ou em várias iterações. A diferença entre eles é que o IJobFor é executado em um único thread, onde IJobForParallelserá dividido entre vários threads.

Eu usarei IJobpara criar um trabalho de operação intensiva IJobFore IJobForParallelpara criar um trabalho que moverá vários inimigos ao redor; isso é apenas para que possamos ver os diferentes impactos no desempenho. Esses trabalhos serão idênticos às tarefas e métodos que criamos anteriormente:

public struct PerformanceIntensiveJob : IJob { }
public struct MoveEnemyJob: IJobFor { }
public struct MoveEnemyParallelJob : IJobParallelFor { }

Adicione as variáveis ​​de membro. No meu caso, o meu IJobnão precisa de nenhum. O IJobFore IJobParallelForprecisa de um float para o tempo delta atual, pois os trabalhos não têm um conceito de quadro; eles operam fora do Unity MonoBehaviour. Eles também precisam de uma matriz de float3para a posição e uma matriz para a velocidade de movimento no eixo y:

public struct MoveEnemyJob : IJobFor
{
   public NativeArray<float3> positions;
   public NativeArray<float> moveYs;
   public float deltaTime; 
}
public struct MoveEnemyParallelJob : IJobParallelFor
{
   public NativeArray<float3> positions;
   public NativeArray<float> moveYs;
   public float deltaTime;
}

A última etapa é implementar o Executemétodo necessário. O IJobFore IJobForParallelambos exigem um intpara o índice da iteração atual que o trabalho está executando.

A diferença é que ao invés de acessar o inimigo transforme se mover, usamos o array que está no trabalho:

public struct PerformanceIntensiveJob : IJob
{
   #region Implementation of IJob

   /// <inheritdoc />
   public void Execute()
   {
       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < 50000; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }

   #endregion
}

// MoveEnemyJob and MoveEnemyParallelJob have the same exact Execute Method. 

   /// <inheritdoc />
   public void Execute(int index)
   {
       positions[index] += new float3(0, moveYs[index] * deltaTime, 0);
       if (positions[index].y > 5f)
           moveYs[index] = -math.abs(moveYs[index]);
       if (positions[index].y < -5f)
           moveYs[index] = +math.abs(moveYs[index]);

       // Represents a Performance Intensive Method like some pathfinding or really complex calculation.
       float value = 0f;
       for (int i = 0; i < 1000; i++)
       {
           value = math.exp10(math.sqrt(value));
       }
   }   private JobHandle PerformanceIntensiveMethodJob()
   {
       PerformanceIntensiveJob job = new PerformanceIntensiveJob();
       return job.Schedule();
   }

Agendando um trabalho

Primeiro, precisamos instalar o trabalho e preencher os dados dos trabalhos:

NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);

MyJob jobData = new MyJob();
jobData.myFloat = result;

Em seguida, agendamos o trabalho com JobHandle jobHandle = jobData.Schedule();. O Schedulemétodo retorna um JobHandleque pode ser usado posteriormente.

Não podemos agendar um trabalho de dentro de um trabalho. Podemos, no entanto, criar novos trabalhos e preencher seus dados de dentro de um trabalho. Depois que um trabalho é agendado, ele não pode ser interrompido.

O trabalho de alto desempenho

Eu criei um método que cria um novo trabalho e o agenda. Ele retorna o identificador de trabalho que posso usar no meu updatemétodo:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   ....

   private JobHandle PerformanceIntensiveMethodJob()
   {
       PerformanceIntensiveJob job = new PerformanceIntensiveJob();
       return job.Schedule();
   }
}

Eu adicionei o trabalho ao meu enum. Então, no Updatemétodo, eu adiciono o caseà switchseção. Eu criei uma matriz de JobHandles. Em seguida, faço um loop por todos os objetos de jogo simulados, adicionando um trabalho agendado para cada um ao array:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   private enum MethodType
   {
       Normal,
       Task,
       Coroutine,
       Job
   }

   ...
   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Job:
               NativeArray<JobHandle> jobHandles =
                   new NativeArray<JobHandle>(numberGameObjectsToImitate, Allocator.Temp);
               for (int i = 0; i < numberGameObjectsToImitate; i++)
                   jobHandles[i] = PerformanceIntensiveMethodJob();
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       ...
   }
}

O MoveEnemyeMoveEnemyParallelJob

Em seguida, adicionei os trabalhos ao meu enum. Então, no Updatemétodo, chamo um novo MoveEnemyJobmétodo, passando o tempo delta. Normalmente você usaria o JobForou o JobParallelFor:

public class PerformanceTaskJob : MonoBehaviour
{
   private enum MethodType
   {
       NormalMoveEnemy,
       TaskMoveEnemy,
       MoveEnemyJob,
       MoveEnemyParallelJob
   }

   ...

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.MoveEnemyJob:
           case MethodType.MoveEnemyParallelJob:
               MoveEnemyJob(Time.deltaTime);
               break;
           default:
               MoveEnemy();
               break;
       }

       ...
   }

   ...

A primeira coisa que faço é definir um array para as posições e um array para o moveYque vou passar para os jobs. Eu então preencho essas matrizes com os dados dos inimigos. Em seguida, crio o trabalho e defino os dados do trabalho dependendo de qual trabalho quero usar. Depois disso, agendo o trabalho dependendo do trabalho que quero usar e do tipo de agendamento que quero fazer:

private void MoveEnemyJob(float deltaTime)
   {
       NativeArray<float3> positions = new NativeArray<float3>(m_enemies.Count, Allocator.TempJob);
       NativeArray<float> moveYs = new NativeArray<float>(m_enemies.Count, Allocator.TempJob);

       for (int i = 0; i < m_enemies.Count; i++)
       {
           positions[i] = m_enemies[i].transform.position;
           moveYs[i] = m_enemies[i].moveY;
       }

       // Use one or the other
       if (method == MethodType.MoveEnemyJob)
       {
           MoveEnemyJob job = new MoveEnemyJob
           {
               deltaTime = deltaTime,
               positions = positions,
               moveYs = moveYs
           };

           // typically we would use one of these methods
           switch (moveEnemyJobType)
           {
               case MoveEnemyJobType.ImmediateMainThread:
                   // Schedule job to run immediately on main thread.
                   // typically would not use.
                   job.Run(m_enemies.Count);
                   break;
               case MoveEnemyJobType.ScheduleSingleWorkerThread:
               case MoveEnemyJobType.ScheduleParallelWorkerThreads:
               {
                   // Schedule job to run at a later point on a single worker thread.
                   // First parameter is how many for-each iterations to perform.
                   // The second parameter is a JobHandle to use for this job's dependencies.
                   //   Dependencies are used to ensure that a job executes on worker threads after the dependency has completed execution.
                   //   In this case we don't need our job to depend on anything so we can use a default one.
                   JobHandle scheduleJobDependency = new JobHandle();
                   JobHandle scheduleJobHandle = job.Schedule(m_enemies.Count, scheduleJobDependency);

                   switch (moveEnemyJobType)
                   {
                       case MoveEnemyJobType.ScheduleSingleWorkerThread:
                           break;
                       case MoveEnemyJobType.ScheduleParallelWorkerThreads:
                       {
                           // Schedule job to run on parallel worker threads.
                           // First parameter is how many for-each iterations to perform.
                           // The second parameter is the batch size,
                           //   essentially the no-overhead inner-loop that just invokes Execute(i) in a loop.
                           //   When there is a lot of work in each iteration then a value of 1 can be sensible.
                           //   When there is very little work values of 32 or 64 can make sense.
                           // The third parameter is a JobHandle to use for this job's dependencies.
                           //   Dependencies are used to ensure that a job executes on worker threads after the dependency has completed execution.
                           JobHandle scheduleParallelJobHandle =
                               job.ScheduleParallel(m_enemies.Count, m_enemies.Count / 10, scheduleJobHandle);

                           break;
                       }
                   }

                   break;
               }
           }
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           MoveEnemyParallelJob job = new MoveEnemyParallelJob
           {
               deltaTime = deltaTime,
               positions = positions,
               moveYs = moveYs
           };

           // Schedule a parallel-for job. First parameter is how many for-each iterations to perform.
           // The second parameter is the batch size,
           // essentially the no-overhead inner-loop that just invokes Execute(i) in a loop.
           // When there is a lot of work in each iteration then a value of 1 can be sensible.
           // When there is very little work values of 32 or 64 can make sense.
           JobHandle jobHandle = job.Schedule(m_enemies.Count, m_enemies.Count / 10);

           }
   }

Obtendo os dados de volta de um trabalho

Temos que esperar que o trabalho seja concluído. Podemos obter o status do JobHandleque usamos quando agendamos o trabalho para concluí-lo. Isso aguardará a conclusão do trabalho antes de continuar a execução: > handle.Complete();ou JobHandle.CompleteAll(jobHandles). Quando o trabalho estiver concluído, o NativeContainerque usamos para configurar o trabalho terá todos os dados que precisamos usar. Uma vez que recuperamos os dados deles, temos que descartá-los adequadamente.

O trabalho de alto desempenho

Isso é bem simples, pois não estou lendo ou gravando nenhum dado no trabalho. Aguardo todos os trabalhos que foram agendados para serem concluídos e descarto a Nativematriz:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   ...

   private async void Update()
   {
       float startTime = Time.realtimeSinceStartup;

       switch (method)
       {
           ...
           case MethodType.Job:
               ....
               JobHandle.CompleteAll(jobHandles);
               jobHandles.Dispose();
               break;
           default:
               Performance.PerformanceIntensiveMethod(50000);
               break;
       }

       ...
   }
}

O trabalho intensivo leva cerca de 6ms para ser concluído com o jogo rodando a cerca de 90 FPS.

Trabalho intensivo

O MoveEnemytrabalho

Eu adiciono as verificações completas apropriadas:

   private void MoveEnemyJob(float deltaTime)
   {
      ....

       if (method == MethodType.MoveEnemyJob)
       {
          ....

           switch (moveEnemyJobType)
           {
               case MoveEnemyJobType.ScheduleSingleWorkerThread:
               case MoveEnemyJobType.ScheduleParallelWorkerThreads:
               {
                   ....

                   // typically one or the other
                   switch (moveEnemyJobType)
                   {
                       case MoveEnemyJobType.ScheduleSingleWorkerThread:
                           scheduleJobHandle.Complete();
                           break;
                       case MoveEnemyJobType.ScheduleParallelWorkerThreads:
                       {
                           scheduleParallelJobHandle.Complete();
                           break;
                       }
                   }

                   break;
               }
           }
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           ....

          jobHandle.Complete();
       }
   }

Após a verificação do tipo de método, eu percorro todos os inimigos, definindo suas transformposições e moveYos dados que foram definidos no trabalho. Em seguida, descarto adequadamente os arrays nativos:

private void MoveEnemyJob(float deltaTime)
   {
      ....

       if (method == MethodType.MoveEnemyJob)
       {
          ....
       }
       else if (method == MethodType.MoveEnemyParallelJob)
       {
           ....
       }

       for (int i = 0; i < m_enemies.Count; i++)
       {
           m_enemies[i].transform.position = positions[i];
           m_enemies[i].moveY = moveYs[i];
       }

       // Native arrays must be disposed manually.
       positions.Dispose();
       moveYs.Dispose();
   }

Exibir e mover mil inimigos com trabalho levou cerca de 160ms com uma taxa de quadros de cerca de 7 FPS sem ganhos de desempenho.

Sem Ganhos de Desempenho

Exibir e mover mil inimigos com trabalho paralelo levou cerca de 30ms com uma taxa de quadros de cerca de 30 FPS.

Trabalho paralelo

O que é o compilador burst no Unity?

O compilador de intermitência é um compilador que traduz de bytecode para código nativo. Usar isso com o C# Job System melhora a qualidade do código gerado, proporcionando um aumento significativo no desempenho, além de reduzir o consumo da bateria em dispositivos móveis.

Para usar isso, basta informar ao Unity que deseja usar a compilação de rajada no trabalho com o [BurstCompile]atributo:

using Unity.Burst;
using Unity.Jobs;
using Unity.Mathematics;

[BurstCompile]
public struct PerformanceIntensiveJob : IJob
{
   ...
}


using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;

[BurstCompile]
public struct MoveEnemyJob : IJobFor
{
   ...
}
[BurstCompile]
public struct MoveEnemyParallelJob : IJobParallelFor
{
   ...
}

Em seguida, no Unity, selecione Jobs > Burst > Enable Completion

Ativar conclusão

Burst é Just-In-Time (JIT) enquanto estiver no Editor, o que significa que isso pode ser desativado enquanto estiver no modo Play. Quando você compila seu projeto, ele é Ahead-Of-Time (AOT), o que significa que isso precisa ser ativado antes de compilar seu projeto. Você pode fazer isso editando a seção Burst AOT Settings na janela Project Settings .

Configurações AOT de rajada

Para obter mais detalhes sobre o compilador de intermitência, consulte a documentação do Unity .

Um trabalho de alto desempenho com o compilador de intermitência

Um trabalho intensivo com rajada leva cerca de 3ms para ser concluído com o jogo rodando a cerca de 150 FPS.

Trabalho intensivo com explosão

Exibindo e movendo mil inimigos, o trabalho com burst levou cerca de 30ms com uma taxa de quadros de cerca de 30 FPS.

Explosão 30 ms

Exibindo e movendo mil inimigos, o trabalho paralelo com burst levou cerca de 6ms com uma taxa de quadros de cerca de 80 a 90 FPS.

6 ms

Conclusão

Podemos usar Taskpara aumentar o desempenho de nossos aplicativos Unity, mas há várias desvantagens em usá-los. É melhor usar as coisas que vêm empacotadas no Unity dependendo do que queremos fazer. Use corrotinas se quisermos esperar que algo termine de carregar de forma assíncrona; podemos iniciar a corrotina e não interromper a execução do processo do nosso programa.

Podemos usar o C# Job System com o compilador de intermitência para obter um enorme ganho de desempenho sem precisar se preocupar com todas as coisas de gerenciamento de threads ao executar tarefas de processo intenso. Usando os sistemas embutidos, temos certeza de que é feito de maneira segura que não causa erros ou bugs indesejados.

As tarefas foram executadas um pouco melhor do que os trabalhos sem usar o compilador de rajadas, mas isso se deve à pequena sobrecarga extra nos bastidores para configurar tudo com segurança para nós. Ao usar o compilador de intermitência, nossos trabalhos executavam nossas tarefas. Quando você precisar de todo o desempenho extra que pode obter, use o C# Job System com burst.

Os arquivos de projeto para isso podem ser encontrados no meu GitHub .

Fonte: https://blog.logrocket.com/performance-unity-async-await-tasks-coroutines-c-job-system-burst-compiler/

 #csharp #async #await