Thierry  Perret

Thierry Perret

1656378000

Qu'est-ce qu'Async, Await et Task en C#

Les performances sont primordiales lorsque vous essayez de publier sur le Web, sur mobile, sur des consoles et même sur certains PC bas de gamme. Un jeu ou une application fonctionnant à moins de 30 FPS peut être source de frustration pour les utilisateurs. Jetons un coup d'œil à certaines des choses que nous pouvons utiliser pour augmenter les performances en réduisant la charge sur le processeur.

Dans cet article, nous expliquerons ce que sont async, awaitet Tasken C # et comment les utiliser dans Unity pour gagner en performance dans votre projet. Ensuite, nous examinerons certains des packages intégrés à Unity : les coroutines, le système de tâches C# et le compilateur en rafale. Nous verrons ce qu'ils sont, comment les utiliser et comment ils augmentent les performances de votre projet.

Pour démarrer ce projet, j'utiliserai Unity 2021.3.4f1. Je n'ai testé ce code sur aucune autre version de Unity ; tous les concepts ici devraient fonctionner sur n'importe quelle version d'Unity après Unity 2019.3. Vos résultats de performances peuvent différer si vous utilisez une version plus ancienne, car Unity a apporté des améliorations significatives avec le modèle de programmation async/wait en 2021. En savoir plus à ce sujet dans le blog Unity d'Unity et .NET, quelle est la prochaine étape , en particulier la section intitulée "Modernizing the Exécution de l'unité.

J'ai créé un nouveau projet Core 2D (URP), mais vous pouvez l'utiliser dans n'importe quel type de projet que vous aimez.

J'ai un sprite que j'ai obtenu de Space Shooter (Redux, plus polices et sons) de Kenney Vleugels .

J'ai créé un préfabriqué ennemi qui contient un Sprite Render et un composant ennemi. Le composant ennemi est un MonoBehaviourqui a un Transformet un floatpour garder une trace de la position et de la vitesse de déplacement sur l'axe y :

using UnityEngine;

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

Qu'est -ce que async, await, et Tasksont en C#

Qu'est-ce que c'est async?

En C#, les méthodes peuvent avoir un mot- asyncclé devant elles, ce qui signifie que les méthodes sont des méthodes asynchrones. C'est juste une façon de dire au compilateur que nous voulons pouvoir exécuter du code à l'intérieur et permettre à l'appelant de cette méthode de continuer l'exécution en attendant que cette méthode se termine.

Un exemple de ceci serait la préparation d'un repas. Vous commencerez à cuire la viande, et pendant que la viande cuit et que vous attendez qu'elle se termine, vous commencerez à faire les côtés. Pendant que les aliments cuisent, vous commencez à mettre la table. Un exemple de ceci dans le code serait static async Task<Steak> MakeSteak(int number).

Unity possède également toutes sortes de méthodes intégrées que vous pouvez appeler de manière asynchrone ; voir les docs Unity pour une liste des méthodes. Avec la façon dont Unity gère la gestion de la mémoire, il utilise soit des coroutines , AsyncOperationsoit le système de tâches C# .

Qu'est-ce que c'est awaitet comment l'utiliser ?

En C#, vous pouvez attendre la fin d'une opération asynchrone en utilisant le mot- awaitclé. Ceci est utilisé à l'intérieur de toute méthode qui a le mot- asyncclé pour attendre qu'une opération continue :

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.
}

Consultez les documents Microsoft pour en savoir plus sur await.

Qu'est-ce qu'un Tasket comment l'utiliser ?

A Taskest une méthode asynchrone qui effectue une seule opération et ne renvoie pas de valeur. Pour a Taskqui renvoie une valeur, nous utiliserions Task<TResult>.

Pour utiliser une tâche, nous la créons comme créer n'importe quel nouvel objet en C# : Task t1 = new Task(void Action). Ensuite, nous commençons la tâche t1.wait. Enfin, nous attendons que la tâche se termine avec t1.wait.

Il existe plusieurs façons de créer, démarrer et exécuter des tâches. Task t2 = Task.Run(void Action)va créer et démarrer une tâche. await Task.Run(void Action)créera, démarrera et attendra la fin de la tâche. Nous pouvons utiliser la méthode alternative la plus courante avec Task t3 = Task.Factory.Start(void Action).

Il existe plusieurs façons d'attendre que la tâche soit terminée. int index = Task.WaitAny(Task[])attendra la fin de toute tâche et nous donnera l'index de la tâche terminée dans le tableau. await Task.WaitAll(Task[])attendra que toutes les tâches soient terminées.

Pour plus d'informations sur les tâches, consultez les documents Microsoft .

Un exemple simpletask

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");
}

Comment la tâche affecte les performances

Comparons maintenant les performances d'une tâche par rapport aux performances d'une méthode.

J'aurai besoin d'une classe statique que je peux utiliser dans toutes mes vérifications de performances. Il aura une méthode et une tâche qui simulent une opération gourmande en performances. La méthode et la tâche effectuent exactement la même opération :

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));
           }
       });
   }
}

Maintenant, j'ai besoin d'un MonoBehaviourque je peux utiliser pour tester l'impact des performances sur la tâche et la méthode. Juste pour que je puisse voir un meilleur impact sur les performances, je vais prétendre que je veux exécuter cela sur dix objets de jeu différents. Je garderai également une trace du temps Updatenécessaire à l'exécution de la méthode.

Dans Update, j'obtiens l'heure de début. Si je teste la méthode, je parcours tous les objets de jeu simulés et j'appelle la méthode intensive en performances. Si je teste la tâche, je crée une nouvelle Taskboucle de tableau à travers tous les objets de jeu simulés et j'ajoute la tâche gourmande en performances au tableau de tâches. Je puis awaitpour toutes les tâches à accomplir. En dehors de la vérification du type de méthode, je mets à jour l'heure de la méthode, en la convertissant en ms. Je le connecte également.

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");
   }
}

La méthode intensive prend environ 65 ms pour se terminer avec le jeu fonctionnant à environ 12 FPS.

Méthode intensiveMéthode intensiveMéthode intensive

La tâche intensive prend environ 4 ms et le jeu tourne à environ 200 FPS.

Tâche intensive

Essayons ceci avec mille ennemis :

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;
  }

L'affichage et le déplacement d'un millier d'ennemis avec la méthode ont pris environ 150 ms avec une fréquence d'images d'environ 7 FPS.

Mille ennemis

Afficher et déplacer un millier d'ennemis avec une tâche a pris environ 50 ms avec une fréquence d'images d'environ 30 FPS.

Affichage des ennemis en mouvement

Pourquoi pas useTasks?

Les tâches sont extrêmement performantes et réduisent la pression sur les performances de votre système. Vous pouvez même les utiliser dans plusieurs threads à l'aide de la bibliothèque parallèle de tâches (TPL).

Il y a cependant quelques inconvénients à les utiliser dans Unity. Le principal inconvénient de l'utilisation Taskdans Unity est qu'ils s'exécutent tous sur le Mainthread. Oui, nous pouvons les faire fonctionner sur d'autres threads, mais Unity fait déjà sa propre gestion des threads et de la mémoire, et vous pouvez créer des erreurs en créant plus de threads que de cœurs de processeur, ce qui entraîne une concurrence pour les ressources.

Les tâches peuvent également être difficiles à exécuter correctement et à déboguer. Lors de l'écriture du code d'origine, je me suis retrouvé avec toutes les tâches en cours d'exécution, mais aucun des ennemis ne s'est déplacé à l'écran. Il a fini par être que j'avais besoin de retourner le Task[]que j'ai créé dans le fichier Task.

Les tâches créent beaucoup de déchets qui affectent les performances. Ils n'apparaissent pas non plus dans le profileur, donc si vous en avez un qui affecte les performances, il est difficile de le retrouver. De plus, j'ai remarqué que parfois mes tâches et mes fonctions de mise à jour continuent de s'exécuter à partir d'autres scènes.

Coroutines d'unité

Selon Unity , "Une coroutine est une fonction qui peut suspendre son exécution (rendement) jusqu'à ce que l' instruction YieldInstruction donnée se termine."

Cela signifie que nous pouvons exécuter du code et attendre la fin d'une tâche avant de continuer. Cela ressemble beaucoup à une méthode asynchrone. Il utilise un type de retour IEnumeratoret we yield returnau lieu de await.

Unity a plusieurs types d' instructions de rendement que nous pouvons utiliser, c'est-à-dire , WaitForSeconds, ou .WaitForEndOfFrameWaitUntilWaitWhile

Pour démarrer les coroutines, nous avons besoin de a MonoBehaviouret utilisons le MonoBehaviour.StartCoroutine.

Pour arrêter une coroutine avant qu'elle ne se termine, nous utilisons MonoBehaviour.StopCoroutine. Lorsque vous arrêtez des coroutines, assurez-vous d'utiliser la même méthode que celle que vous avez utilisée pour la démarrer.

Les cas d'utilisation courants des coroutines dans Unity consistent à attendre que les actifs se chargent et à créer des temporisateurs de temps de recharge.

Exemple : Un chargeur de scène utilisant une coroutine

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);
   }
}

Vérifier l'impact d'une coroutine sur les performances

Voyons comment l'utilisation d'une coroutine impacte les performances de notre projet. Je ne vais le faire qu'avec la méthode intensive en performances.

J'ai ajouté le Coroutineà l' MethodTypeénumération et aux variables pour garder une trace de son état:

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

   ...

   private Coroutine m_performanceCoroutine;

J'ai créé la coroutine. Ceci est similaire à la tâche et à la méthode gourmandes en performances que nous avons créées précédemment avec du code ajouté pour mettre à jour l'heure de la méthode :

   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;
   }

Dans la Updateméthode, j'ai ajouté la vérification de la coroutine. J'ai également modifié l'heure de la méthode, mis à jour le code et ajouté du code pour arrêter la coroutine si elle était en cours d'exécution et nous avons changé le type de méthode :

   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 coroutine intensive prend environ 6 ms pour se terminer avec le jeu fonctionnant à environ 90 FPS.

Coroutine intensive

Le système de tâches C# et le compilateur en rafale

Qu'est-ce que le système de tâches C# ?

Le système de tâches C# est l'implémentation par Unity de tâches faciles à écrire, qui ne génèrent pas les déchets générés par les tâches et utilisent les threads de travail que Unity a déjà créés. Cela corrige tous les inconvénients des tâches.

Unity compare les travaux en tant que threads, mais ils disent qu'un travail effectue une tâche spécifique. Les tâches peuvent également dépendre d'autres tâches à terminer avant de s'exécuter ; cela résout le problème avec la tâche que j'avais qui ne s'est pas correctement déplacée Unitsparce qu'elle dépendait d'une autre tâche à terminer en premier.

Les dépendances de travail sont automatiquement prises en charge pour nous par Unity. Le système d'emploi dispose également d'un système de sécurité intégré principalement pour se protéger contre les conditions de course . Une mise en garde avec les travaux est qu'ils ne peuvent contenir que des variables membres qui sont soit des types blittables, soit des types NativeContainer ; c'est un inconvénient du système de sécurité.

Pour utiliser le système de travail, vous créez le travail, planifiez le travail, attendez que le travail se termine, puis utilisez les données renvoyées par le travail. Le système de tâches est nécessaire pour utiliser la pile technologique orientée données (DOTS) de Unity.

Pour plus de détails sur le système de tâches, consultez la documentation Unity .

Création d'un emploi

Pour créer un travail, vous créez un stuctqui implémente l'une des IJobinterfaces ( IJob , IJobFor , IJobParallelFor , Unity.Engine.Jobs.IJobParallelForTransform ). IJobest un travail de base. IJobForet IJobForParallelpermettent d'effectuer la même opération sur chaque élément d'un conteneur natif ou pour plusieurs itérations. La différence entre eux est que le IJobFor s'exécute sur un seul thread où le IJobForParallelsera divisé entre plusieurs threads.

Je vais l'utiliser IJobpour créer un travail d'opération intensive IJobForet IJobForParallelpour créer un travail qui déplacera plusieurs ennemis; c'est juste pour que nous puissions voir les différents impacts sur les performances. Ces tâches seront identiques aux tâches et méthodes que nous avons créées précédemment :

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

Ajoutez les variables membres. Dans mon cas, mon IJobn'en a pas besoin. Le IJobForet IJobParallelForont besoin d'un flottant pour le temps delta actuel car les travaux n'ont pas de concept de cadre ; ils opèrent en dehors de Unity MonoBehaviour. Ils ont également besoin d'un tableau de float3pour la position et d'un tableau pour la vitesse de déplacement sur l'axe 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;
}

La dernière étape consiste à mettre en œuvre la Executeméthode requise. Le IJobForet IJobForParallelles deux nécessitent un intpour l'index de l'itération actuelle que le travail est en train d'exécuter.

La différence est qu'au lieu d'accéder à l'ennemi transformet de se déplacer, nous utilisons le tableau qui se trouve dans le travail :

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

Planification d'un travail

Tout d'abord, nous devons établir le travail et remplir les données des travaux :

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

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

Ensuite, nous planifions le travail avec JobHandle jobHandle = jobData.Schedule();. La Scheduleméthode retourne un JobHandlequi peut être utilisé plus tard.

Nous ne pouvons pas planifier une tâche à partir d'une tâche. Cependant, nous pouvons créer de nouvelles tâches et remplir leurs données à partir d'une tâche. Une fois qu'une tâche a été planifiée, elle ne peut pas être interrompue.

Le travail à haute performance

J'ai créé une méthode qui crée un nouveau travail et le planifie. Il renvoie le descripteur de travail que je peux utiliser dans ma updateméthode :

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   ....

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

J'ai ajouté le travail à mon énumération. Ensuite, dans la Updateméthode, j'ajoute le caseà la switchsection. J'ai créé un tableau de JobHandles. Je parcours ensuite tous les objets de jeu simulés, en ajoutant une tâche planifiée pour chacun au tableau :

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;
       }

       ...
   }
}

Le MoveEnemyetMoveEnemyParallelJob

Ensuite, j'ai ajouté les emplois à mon énumération. Puis dans la Updateméthode, j'appelle une nouvelle MoveEnemyJobméthode, en passant le temps delta. Normalement, vous utiliseriez soit le JobForou le 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;
       }

       ...
   }

   ...

La première chose que je fais est de définir un tableau pour les positions et un tableau pour les moveYque je transmettrai aux tâches. Je remplis ensuite ces tableaux avec les données des ennemis. Ensuite, je crée le travail et définit les données du travail en fonction du travail que je souhaite utiliser. Après cela, je planifie le travail en fonction du travail que je veux utiliser et du type de planification que je veux faire :

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);

           }
   }

Récupérer les données d'une tâche

Nous devons attendre que le travail soit terminé. Nous pouvons obtenir le statut à partir du JobHandleque nous avons utilisé lorsque nous avons programmé le travail pour le terminer. Cela attendra que le travail soit terminé avant de poursuivre l'exécution : > handle.Complete();ou JobHandle.CompleteAll(jobHandles). Une fois le travail terminé, le NativeContainerque nous avons utilisé pour configurer le travail aura toutes les données que nous devons utiliser. Une fois que nous en avons récupéré les données, nous devons en disposer correctement.

Le travail à haute performance

C'est assez simple puisque je ne suis pas en train de lire ou d'écrire des données dans le travail. J'attends que tous les travaux programmés soient terminés, puis je supprime le Nativetableau :

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;
       }

       ...
   }
}

Le travail intensif prend environ 6 ms et le jeu tourne à environ 90 FPS.

Travail intensif

Le MoveEnemytravail

J'ajoute les vérifications complètes appropriées :

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

Après les vérifications du type de méthode, je parcoure tous les ennemis, en définissant leurs transformpositions et moveYles données qui ont été définies dans le travail. Ensuite, je dispose correctement des tableaux natifs :

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

L'affichage et le déplacement d'un millier d'ennemis avec le travail ont pris environ 160 ms avec une fréquence d'images d'environ 7 FPS sans gain de performances.

Aucun gain de performances

L'affichage et le déplacement d'un millier d'ennemis avec un travail parallèle ont pris environ 30 ms avec une fréquence d'images d'environ 30 FPS.

Travail parallèle

Qu'est-ce que le compilateur de rafales dans Unity ?

Le compilateur en rafale est un compilateur qui traduit du bytecode en code natif. L'utiliser avec le système de tâches C # améliore la qualité du code généré, vous donnant une augmentation significative des performances ainsi qu'une réduction de la consommation de la batterie sur les appareils mobiles.

Pour l'utiliser, il vous suffit d'indiquer à Unity que vous souhaitez utiliser la compilation en rafale sur le travail avec l' [BurstCompile]attribut :

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
{
   ...
}

Ensuite, dans Unity, sélectionnez Jobs > Burst > Enable Completion

Activer l'achèvement

Burst est juste à temps (JIT) dans l'éditeur, ce qui signifie qu'il peut être désactivé en mode lecture. Lorsque vous construisez votre projet, c'est Ahead-Of-Time (AOT), ce qui signifie que cela doit être activé avant de construire votre projet. Vous pouvez le faire en modifiant la section Burst AOT Settings dans la fenêtre Project Settings .

Paramètres AOT en rafale

Pour plus de détails sur le compilateur de rafale, consultez la documentation Unity .

Un travail gourmand en performances avec le compilateur burst

Un travail intensif avec rafale prend environ 3 ms pour se terminer avec le jeu fonctionnant à environ 150 FPS.

Travail intensif avec rafale

Affichage et déplacement d'un millier d'ennemis, le travail avec rafale a pris environ 30 ms avec une fréquence d'images d'environ 30 FPS.

Rafale 30 ms

Affichage et déplacement d'un millier d'ennemis, le travail parallèle à la rafale a pris environ 6 ms avec une fréquence d'images d'environ 80 à 90 FPS.

6 millisecondes

Conclusion

Nous pouvons utiliser Taskpour augmenter les performances de nos applications Unity, mais leur utilisation présente plusieurs inconvénients. Il est préférable d'utiliser les éléments fournis dans Unity en fonction de ce que nous voulons faire. Utilisez des coroutines si nous voulons attendre que quelque chose finisse de se charger de manière asynchrone ; nous pouvons démarrer la coroutine et ne pas arrêter l'exécution du processus de notre programme.

Nous pouvons utiliser le système de tâches C # avec le compilateur en rafale pour obtenir un gain de performances considérable sans avoir à nous soucier de toutes les tâches de gestion des threads lors de l'exécution de tâches gourmandes en processus. En utilisant les systèmes intégrés, nous sommes sûrs que cela est fait d'une manière sûre qui ne cause pas d'erreurs ou de bugs indésirables.

Les tâches s'exécutaient un peu mieux que les travaux sans utiliser le compilateur en rafale, mais cela est dû au peu de surcharge supplémentaire dans les coulisses pour tout configurer en toute sécurité pour nous. Lors de l'utilisation du compilateur de rafale, nos travaux ont effectué nos tâches. Lorsque vous avez besoin de toutes les performances supplémentaires que vous pouvez obtenir, utilisez le système de tâches C# avec burst.

Les fichiers de projet pour cela peuvent être trouvés sur mon GitHub .

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

 #csharp #async #await 

What is GEEK

Buddha Community

Qu'est-ce qu'Async, Await et Task en C#
Thierry  Perret

Thierry Perret

1656378000

Qu'est-ce qu'Async, Await et Task en C#

Les performances sont primordiales lorsque vous essayez de publier sur le Web, sur mobile, sur des consoles et même sur certains PC bas de gamme. Un jeu ou une application fonctionnant à moins de 30 FPS peut être source de frustration pour les utilisateurs. Jetons un coup d'œil à certaines des choses que nous pouvons utiliser pour augmenter les performances en réduisant la charge sur le processeur.

Dans cet article, nous expliquerons ce que sont async, awaitet Tasken C # et comment les utiliser dans Unity pour gagner en performance dans votre projet. Ensuite, nous examinerons certains des packages intégrés à Unity : les coroutines, le système de tâches C# et le compilateur en rafale. Nous verrons ce qu'ils sont, comment les utiliser et comment ils augmentent les performances de votre projet.

Pour démarrer ce projet, j'utiliserai Unity 2021.3.4f1. Je n'ai testé ce code sur aucune autre version de Unity ; tous les concepts ici devraient fonctionner sur n'importe quelle version d'Unity après Unity 2019.3. Vos résultats de performances peuvent différer si vous utilisez une version plus ancienne, car Unity a apporté des améliorations significatives avec le modèle de programmation async/wait en 2021. En savoir plus à ce sujet dans le blog Unity d'Unity et .NET, quelle est la prochaine étape , en particulier la section intitulée "Modernizing the Exécution de l'unité.

J'ai créé un nouveau projet Core 2D (URP), mais vous pouvez l'utiliser dans n'importe quel type de projet que vous aimez.

J'ai un sprite que j'ai obtenu de Space Shooter (Redux, plus polices et sons) de Kenney Vleugels .

J'ai créé un préfabriqué ennemi qui contient un Sprite Render et un composant ennemi. Le composant ennemi est un MonoBehaviourqui a un Transformet un floatpour garder une trace de la position et de la vitesse de déplacement sur l'axe y :

using UnityEngine;

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

Qu'est -ce que async, await, et Tasksont en C#

Qu'est-ce que c'est async?

En C#, les méthodes peuvent avoir un mot- asyncclé devant elles, ce qui signifie que les méthodes sont des méthodes asynchrones. C'est juste une façon de dire au compilateur que nous voulons pouvoir exécuter du code à l'intérieur et permettre à l'appelant de cette méthode de continuer l'exécution en attendant que cette méthode se termine.

Un exemple de ceci serait la préparation d'un repas. Vous commencerez à cuire la viande, et pendant que la viande cuit et que vous attendez qu'elle se termine, vous commencerez à faire les côtés. Pendant que les aliments cuisent, vous commencez à mettre la table. Un exemple de ceci dans le code serait static async Task<Steak> MakeSteak(int number).

Unity possède également toutes sortes de méthodes intégrées que vous pouvez appeler de manière asynchrone ; voir les docs Unity pour une liste des méthodes. Avec la façon dont Unity gère la gestion de la mémoire, il utilise soit des coroutines , AsyncOperationsoit le système de tâches C# .

Qu'est-ce que c'est awaitet comment l'utiliser ?

En C#, vous pouvez attendre la fin d'une opération asynchrone en utilisant le mot- awaitclé. Ceci est utilisé à l'intérieur de toute méthode qui a le mot- asyncclé pour attendre qu'une opération continue :

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.
}

Consultez les documents Microsoft pour en savoir plus sur await.

Qu'est-ce qu'un Tasket comment l'utiliser ?

A Taskest une méthode asynchrone qui effectue une seule opération et ne renvoie pas de valeur. Pour a Taskqui renvoie une valeur, nous utiliserions Task<TResult>.

Pour utiliser une tâche, nous la créons comme créer n'importe quel nouvel objet en C# : Task t1 = new Task(void Action). Ensuite, nous commençons la tâche t1.wait. Enfin, nous attendons que la tâche se termine avec t1.wait.

Il existe plusieurs façons de créer, démarrer et exécuter des tâches. Task t2 = Task.Run(void Action)va créer et démarrer une tâche. await Task.Run(void Action)créera, démarrera et attendra la fin de la tâche. Nous pouvons utiliser la méthode alternative la plus courante avec Task t3 = Task.Factory.Start(void Action).

Il existe plusieurs façons d'attendre que la tâche soit terminée. int index = Task.WaitAny(Task[])attendra la fin de toute tâche et nous donnera l'index de la tâche terminée dans le tableau. await Task.WaitAll(Task[])attendra que toutes les tâches soient terminées.

Pour plus d'informations sur les tâches, consultez les documents Microsoft .

Un exemple simpletask

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");
}

Comment la tâche affecte les performances

Comparons maintenant les performances d'une tâche par rapport aux performances d'une méthode.

J'aurai besoin d'une classe statique que je peux utiliser dans toutes mes vérifications de performances. Il aura une méthode et une tâche qui simulent une opération gourmande en performances. La méthode et la tâche effectuent exactement la même opération :

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));
           }
       });
   }
}

Maintenant, j'ai besoin d'un MonoBehaviourque je peux utiliser pour tester l'impact des performances sur la tâche et la méthode. Juste pour que je puisse voir un meilleur impact sur les performances, je vais prétendre que je veux exécuter cela sur dix objets de jeu différents. Je garderai également une trace du temps Updatenécessaire à l'exécution de la méthode.

Dans Update, j'obtiens l'heure de début. Si je teste la méthode, je parcours tous les objets de jeu simulés et j'appelle la méthode intensive en performances. Si je teste la tâche, je crée une nouvelle Taskboucle de tableau à travers tous les objets de jeu simulés et j'ajoute la tâche gourmande en performances au tableau de tâches. Je puis awaitpour toutes les tâches à accomplir. En dehors de la vérification du type de méthode, je mets à jour l'heure de la méthode, en la convertissant en ms. Je le connecte également.

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");
   }
}

La méthode intensive prend environ 65 ms pour se terminer avec le jeu fonctionnant à environ 12 FPS.

Méthode intensiveMéthode intensiveMéthode intensive

La tâche intensive prend environ 4 ms et le jeu tourne à environ 200 FPS.

Tâche intensive

Essayons ceci avec mille ennemis :

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;
  }

L'affichage et le déplacement d'un millier d'ennemis avec la méthode ont pris environ 150 ms avec une fréquence d'images d'environ 7 FPS.

Mille ennemis

Afficher et déplacer un millier d'ennemis avec une tâche a pris environ 50 ms avec une fréquence d'images d'environ 30 FPS.

Affichage des ennemis en mouvement

Pourquoi pas useTasks?

Les tâches sont extrêmement performantes et réduisent la pression sur les performances de votre système. Vous pouvez même les utiliser dans plusieurs threads à l'aide de la bibliothèque parallèle de tâches (TPL).

Il y a cependant quelques inconvénients à les utiliser dans Unity. Le principal inconvénient de l'utilisation Taskdans Unity est qu'ils s'exécutent tous sur le Mainthread. Oui, nous pouvons les faire fonctionner sur d'autres threads, mais Unity fait déjà sa propre gestion des threads et de la mémoire, et vous pouvez créer des erreurs en créant plus de threads que de cœurs de processeur, ce qui entraîne une concurrence pour les ressources.

Les tâches peuvent également être difficiles à exécuter correctement et à déboguer. Lors de l'écriture du code d'origine, je me suis retrouvé avec toutes les tâches en cours d'exécution, mais aucun des ennemis ne s'est déplacé à l'écran. Il a fini par être que j'avais besoin de retourner le Task[]que j'ai créé dans le fichier Task.

Les tâches créent beaucoup de déchets qui affectent les performances. Ils n'apparaissent pas non plus dans le profileur, donc si vous en avez un qui affecte les performances, il est difficile de le retrouver. De plus, j'ai remarqué que parfois mes tâches et mes fonctions de mise à jour continuent de s'exécuter à partir d'autres scènes.

Coroutines d'unité

Selon Unity , "Une coroutine est une fonction qui peut suspendre son exécution (rendement) jusqu'à ce que l' instruction YieldInstruction donnée se termine."

Cela signifie que nous pouvons exécuter du code et attendre la fin d'une tâche avant de continuer. Cela ressemble beaucoup à une méthode asynchrone. Il utilise un type de retour IEnumeratoret we yield returnau lieu de await.

Unity a plusieurs types d' instructions de rendement que nous pouvons utiliser, c'est-à-dire , WaitForSeconds, ou .WaitForEndOfFrameWaitUntilWaitWhile

Pour démarrer les coroutines, nous avons besoin de a MonoBehaviouret utilisons le MonoBehaviour.StartCoroutine.

Pour arrêter une coroutine avant qu'elle ne se termine, nous utilisons MonoBehaviour.StopCoroutine. Lorsque vous arrêtez des coroutines, assurez-vous d'utiliser la même méthode que celle que vous avez utilisée pour la démarrer.

Les cas d'utilisation courants des coroutines dans Unity consistent à attendre que les actifs se chargent et à créer des temporisateurs de temps de recharge.

Exemple : Un chargeur de scène utilisant une coroutine

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);
   }
}

Vérifier l'impact d'une coroutine sur les performances

Voyons comment l'utilisation d'une coroutine impacte les performances de notre projet. Je ne vais le faire qu'avec la méthode intensive en performances.

J'ai ajouté le Coroutineà l' MethodTypeénumération et aux variables pour garder une trace de son état:

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

   ...

   private Coroutine m_performanceCoroutine;

J'ai créé la coroutine. Ceci est similaire à la tâche et à la méthode gourmandes en performances que nous avons créées précédemment avec du code ajouté pour mettre à jour l'heure de la méthode :

   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;
   }

Dans la Updateméthode, j'ai ajouté la vérification de la coroutine. J'ai également modifié l'heure de la méthode, mis à jour le code et ajouté du code pour arrêter la coroutine si elle était en cours d'exécution et nous avons changé le type de méthode :

   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 coroutine intensive prend environ 6 ms pour se terminer avec le jeu fonctionnant à environ 90 FPS.

Coroutine intensive

Le système de tâches C# et le compilateur en rafale

Qu'est-ce que le système de tâches C# ?

Le système de tâches C# est l'implémentation par Unity de tâches faciles à écrire, qui ne génèrent pas les déchets générés par les tâches et utilisent les threads de travail que Unity a déjà créés. Cela corrige tous les inconvénients des tâches.

Unity compare les travaux en tant que threads, mais ils disent qu'un travail effectue une tâche spécifique. Les tâches peuvent également dépendre d'autres tâches à terminer avant de s'exécuter ; cela résout le problème avec la tâche que j'avais qui ne s'est pas correctement déplacée Unitsparce qu'elle dépendait d'une autre tâche à terminer en premier.

Les dépendances de travail sont automatiquement prises en charge pour nous par Unity. Le système d'emploi dispose également d'un système de sécurité intégré principalement pour se protéger contre les conditions de course . Une mise en garde avec les travaux est qu'ils ne peuvent contenir que des variables membres qui sont soit des types blittables, soit des types NativeContainer ; c'est un inconvénient du système de sécurité.

Pour utiliser le système de travail, vous créez le travail, planifiez le travail, attendez que le travail se termine, puis utilisez les données renvoyées par le travail. Le système de tâches est nécessaire pour utiliser la pile technologique orientée données (DOTS) de Unity.

Pour plus de détails sur le système de tâches, consultez la documentation Unity .

Création d'un emploi

Pour créer un travail, vous créez un stuctqui implémente l'une des IJobinterfaces ( IJob , IJobFor , IJobParallelFor , Unity.Engine.Jobs.IJobParallelForTransform ). IJobest un travail de base. IJobForet IJobForParallelpermettent d'effectuer la même opération sur chaque élément d'un conteneur natif ou pour plusieurs itérations. La différence entre eux est que le IJobFor s'exécute sur un seul thread où le IJobForParallelsera divisé entre plusieurs threads.

Je vais l'utiliser IJobpour créer un travail d'opération intensive IJobForet IJobForParallelpour créer un travail qui déplacera plusieurs ennemis; c'est juste pour que nous puissions voir les différents impacts sur les performances. Ces tâches seront identiques aux tâches et méthodes que nous avons créées précédemment :

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

Ajoutez les variables membres. Dans mon cas, mon IJobn'en a pas besoin. Le IJobForet IJobParallelForont besoin d'un flottant pour le temps delta actuel car les travaux n'ont pas de concept de cadre ; ils opèrent en dehors de Unity MonoBehaviour. Ils ont également besoin d'un tableau de float3pour la position et d'un tableau pour la vitesse de déplacement sur l'axe 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;
}

La dernière étape consiste à mettre en œuvre la Executeméthode requise. Le IJobForet IJobForParallelles deux nécessitent un intpour l'index de l'itération actuelle que le travail est en train d'exécuter.

La différence est qu'au lieu d'accéder à l'ennemi transformet de se déplacer, nous utilisons le tableau qui se trouve dans le travail :

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

Planification d'un travail

Tout d'abord, nous devons établir le travail et remplir les données des travaux :

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

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

Ensuite, nous planifions le travail avec JobHandle jobHandle = jobData.Schedule();. La Scheduleméthode retourne un JobHandlequi peut être utilisé plus tard.

Nous ne pouvons pas planifier une tâche à partir d'une tâche. Cependant, nous pouvons créer de nouvelles tâches et remplir leurs données à partir d'une tâche. Une fois qu'une tâche a été planifiée, elle ne peut pas être interrompue.

Le travail à haute performance

J'ai créé une méthode qui crée un nouveau travail et le planifie. Il renvoie le descripteur de travail que je peux utiliser dans ma updateméthode :

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   ....

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

J'ai ajouté le travail à mon énumération. Ensuite, dans la Updateméthode, j'ajoute le caseà la switchsection. J'ai créé un tableau de JobHandles. Je parcours ensuite tous les objets de jeu simulés, en ajoutant une tâche planifiée pour chacun au tableau :

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;
       }

       ...
   }
}

Le MoveEnemyetMoveEnemyParallelJob

Ensuite, j'ai ajouté les emplois à mon énumération. Puis dans la Updateméthode, j'appelle une nouvelle MoveEnemyJobméthode, en passant le temps delta. Normalement, vous utiliseriez soit le JobForou le 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;
       }

       ...
   }

   ...

La première chose que je fais est de définir un tableau pour les positions et un tableau pour les moveYque je transmettrai aux tâches. Je remplis ensuite ces tableaux avec les données des ennemis. Ensuite, je crée le travail et définit les données du travail en fonction du travail que je souhaite utiliser. Après cela, je planifie le travail en fonction du travail que je veux utiliser et du type de planification que je veux faire :

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);

           }
   }

Récupérer les données d'une tâche

Nous devons attendre que le travail soit terminé. Nous pouvons obtenir le statut à partir du JobHandleque nous avons utilisé lorsque nous avons programmé le travail pour le terminer. Cela attendra que le travail soit terminé avant de poursuivre l'exécution : > handle.Complete();ou JobHandle.CompleteAll(jobHandles). Une fois le travail terminé, le NativeContainerque nous avons utilisé pour configurer le travail aura toutes les données que nous devons utiliser. Une fois que nous en avons récupéré les données, nous devons en disposer correctement.

Le travail à haute performance

C'est assez simple puisque je ne suis pas en train de lire ou d'écrire des données dans le travail. J'attends que tous les travaux programmés soient terminés, puis je supprime le Nativetableau :

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;
       }

       ...
   }
}

Le travail intensif prend environ 6 ms et le jeu tourne à environ 90 FPS.

Travail intensif

Le MoveEnemytravail

J'ajoute les vérifications complètes appropriées :

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

Après les vérifications du type de méthode, je parcoure tous les ennemis, en définissant leurs transformpositions et moveYles données qui ont été définies dans le travail. Ensuite, je dispose correctement des tableaux natifs :

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

L'affichage et le déplacement d'un millier d'ennemis avec le travail ont pris environ 160 ms avec une fréquence d'images d'environ 7 FPS sans gain de performances.

Aucun gain de performances

L'affichage et le déplacement d'un millier d'ennemis avec un travail parallèle ont pris environ 30 ms avec une fréquence d'images d'environ 30 FPS.

Travail parallèle

Qu'est-ce que le compilateur de rafales dans Unity ?

Le compilateur en rafale est un compilateur qui traduit du bytecode en code natif. L'utiliser avec le système de tâches C # améliore la qualité du code généré, vous donnant une augmentation significative des performances ainsi qu'une réduction de la consommation de la batterie sur les appareils mobiles.

Pour l'utiliser, il vous suffit d'indiquer à Unity que vous souhaitez utiliser la compilation en rafale sur le travail avec l' [BurstCompile]attribut :

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
{
   ...
}

Ensuite, dans Unity, sélectionnez Jobs > Burst > Enable Completion

Activer l'achèvement

Burst est juste à temps (JIT) dans l'éditeur, ce qui signifie qu'il peut être désactivé en mode lecture. Lorsque vous construisez votre projet, c'est Ahead-Of-Time (AOT), ce qui signifie que cela doit être activé avant de construire votre projet. Vous pouvez le faire en modifiant la section Burst AOT Settings dans la fenêtre Project Settings .

Paramètres AOT en rafale

Pour plus de détails sur le compilateur de rafale, consultez la documentation Unity .

Un travail gourmand en performances avec le compilateur burst

Un travail intensif avec rafale prend environ 3 ms pour se terminer avec le jeu fonctionnant à environ 150 FPS.

Travail intensif avec rafale

Affichage et déplacement d'un millier d'ennemis, le travail avec rafale a pris environ 30 ms avec une fréquence d'images d'environ 30 FPS.

Rafale 30 ms

Affichage et déplacement d'un millier d'ennemis, le travail parallèle à la rafale a pris environ 6 ms avec une fréquence d'images d'environ 80 à 90 FPS.

6 millisecondes

Conclusion

Nous pouvons utiliser Taskpour augmenter les performances de nos applications Unity, mais leur utilisation présente plusieurs inconvénients. Il est préférable d'utiliser les éléments fournis dans Unity en fonction de ce que nous voulons faire. Utilisez des coroutines si nous voulons attendre que quelque chose finisse de se charger de manière asynchrone ; nous pouvons démarrer la coroutine et ne pas arrêter l'exécution du processus de notre programme.

Nous pouvons utiliser le système de tâches C # avec le compilateur en rafale pour obtenir un gain de performances considérable sans avoir à nous soucier de toutes les tâches de gestion des threads lors de l'exécution de tâches gourmandes en processus. En utilisant les systèmes intégrés, nous sommes sûrs que cela est fait d'une manière sûre qui ne cause pas d'erreurs ou de bugs indésirables.

Les tâches s'exécutaient un peu mieux que les travaux sans utiliser le compilateur en rafale, mais cela est dû au peu de surcharge supplémentaire dans les coulisses pour tout configurer en toute sécurité pour nous. Lors de l'utilisation du compilateur de rafale, nos travaux ont effectué nos tâches. Lorsque vous avez besoin de toutes les performances supplémentaires que vous pouvez obtenir, utilisez le système de tâches C# avec burst.

Les fichiers de projet pour cela peuvent être trouvés sur mon GitHub .

Source : 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

Ari  Bogisich

Ari Bogisich

1589816580

Using isdigit() in C/C++

In this article, we’ll take a look at using the isdigit() function in C/C++. This is a very simple way to check if any value is a digit or not. Let’s look at how to use this function, using some simple examples.

#c programming #c++ #c #c#

Ari  Bogisich

Ari Bogisich

1590587580

Loops in C++ | For, While, and Do While Loops in C++

In this Video We are going to see how to use Loops in C++. We will see How to use For, While, and Do While Loops in C++.
C++ is general purpose, compiled, object-oriented programming language and its concepts served as the basis for several other languages such as Java, Python, Ruby, Perl etc.

#c #c# #c++ #programming-c