Hong  Nhung

Hong Nhung

1656374400

Async, Await Và Task Là Gì Trong C #

Hiệu suất là tất cả mọi thứ khi bạn đang cố gắng xuất bản lên web, thiết bị di động, bảng điều khiển và thậm chí một số PC cấp thấp hơn. Một trò chơi hoặc ứng dụng chạy ở tốc độ dưới 30 FPS có thể gây ra sự thất vọng cho người dùng. Chúng ta hãy xem xét một số thứ chúng ta có thể sử dụng để tăng hiệu suất bằng cách giảm tải cho CPU.

Trong bài đăng này, chúng tôi sẽ đề cập đến những gì async, awaitTasktrong C # là gì và cách sử dụng chúng trong Unity để đạt được hiệu suất trong dự án của bạn. Tiếp theo, chúng ta sẽ xem xét một số gói có sẵn của Unity: coroutines, C # Job System và trình biên dịch bùng nổ. Chúng tôi sẽ xem xét chúng là gì, cách sử dụng chúng và cách chúng tăng hiệu suất trong dự án của bạn.

Để bắt đầu dự án này, tôi sẽ sử dụng Unity 2021.3.4f1. Tôi chưa thử nghiệm mã này trên bất kỳ phiên bản Unity nào khác; tất cả các khái niệm ở đây sẽ hoạt động trên bất kỳ phiên bản Unity nào sau Unity 2019.3. Kết quả hiệu suất của bạn có thể khác nếu sử dụng phiên bản cũ hơn vì Unity đã thực hiện một số cải tiến đáng kể với mô hình lập trình async / await vào năm 2021. Đọc thêm về nó trong blog Unity và .NET của Unity, phần tiếp theo là phần có nhãn “Hiện đại hóa Thời gian chạy thống nhất. ”

Tôi đã tạo một dự án 2D (URP) Core mới, nhưng bạn có thể sử dụng dự án này trong bất kỳ loại dự án nào bạn thích.

Tôi có một sprite mà tôi nhận được từ Space Shooter (Redux, cùng với phông chữ và âm thanh) của Kenney Vleugels .

Tôi đã tạo một nhà lắp ghép của kẻ thù có chứa Sprite Render và một Thành phần của kẻ thù. Thành phần địch là một thành phần MonoBehaviourcó a Transformvà a floatđể theo dõi vị trí và tốc độ di chuyển trên trục y:

using UnityEngine;

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

Cái gì async, awaitTaskcó trong C #

Là gì async?

Trong C #, các phương thức có thể có một asynctừ khóa phía trước, nghĩa là các phương thức đó là phương thức không đồng bộ. Đây chỉ là một cách để nói với trình biên dịch rằng chúng ta muốn có thể thực thi mã bên trong và cho phép người gọi phương thức đó tiếp tục thực thi trong khi chờ phương thức này kết thúc.

Một ví dụ về điều này sẽ là nấu một bữa ăn. Bạn sẽ bắt đầu nấu thịt, và trong khi thịt đang nấu và bạn chờ nó hoàn thành, bạn sẽ bắt đầu làm các mặt. Trong khi thức ăn đang nấu, bạn sẽ bắt đầu dọn bàn ăn. Một ví dụ về điều này trong mã sẽ là static async Task<Steak> MakeSteak(int number).

Unity cũng có tất cả các loại phương thức có sẵn mà bạn có thể gọi không đồng bộ; xem tài liệu Unity để biết danh sách các phương pháp. Với cách Unity xử lý việc quản lý bộ nhớ, nó sử dụng coroutines hoặcAsyncOperation Hệ thống công việc C # .

Bạn awaitsử dụng nó như thế nào và nó là gì?

Trong C #, bạn có thể đợi hoạt động không đồng bộ hoàn tất bằng cách sử dụng awaittừ khóa. Điều này được sử dụng bên trong bất kỳ phương thức nào có asynctừ khóa để chờ một thao tác tiếp tục:

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

Xem các tài liệu của Microsoft để biết thêm về await.

A là gì Taskvà bạn sử dụng nó như thế nào?

A Tasklà một phương thức không đồng bộ thực hiện một thao tác đơn lẻ và không trả về giá trị. Đối với một Tasktrả về một giá trị, chúng tôi sẽ sử dụng Task<TResult>.

Để sử dụng một tác vụ, chúng ta tạo nó giống như tạo bất kỳ đối tượng mới nào trong C # Task t1 = new Task(void Action):. Tiếp theo, chúng ta bắt đầu nhiệm vụ t1.wait. Cuối cùng, chúng tôi đợi nhiệm vụ hoàn thành với t1.wait.

Có một số cách để tạo, bắt đầu và chạy tác vụ. Task t2 = Task.Run(void Action)sẽ tạo và bắt đầu một nhiệm vụ. await Task.Run(void Action)sẽ tạo, bắt đầu và đợi tác vụ hoàn thành. Chúng tôi có thể sử dụng cách thay thế phổ biến nhất với Task t3 = Task.Factory.Start(void Action).

Có một số cách mà chúng ta có thể đợi Tác vụ hoàn thành. int index = Task.WaitAny(Task[])sẽ đợi bất kỳ nhiệm vụ nào hoàn thành và cung cấp cho chúng tôi chỉ số của nhiệm vụ đã hoàn thành trong mảng. await Task.WaitAll(Task[])sẽ đợi tất cả các nhiệm vụ hoàn thành.

Để biết thêm về các tác vụ, hãy xem Tài liệu Microsoft .

Một taskví dụ đơn giản

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

Nhiệm vụ ảnh hưởng như thế nào đến hiệu suất

Bây giờ chúng ta hãy so sánh hiệu suất của một tác vụ so với hiệu suất của một phương pháp.

Tôi sẽ cần một lớp tĩnh mà tôi có thể sử dụng trong tất cả các lần kiểm tra hiệu suất của mình. Nó sẽ có một phương thức và một nhiệm vụ mô phỏng một hoạt động đòi hỏi nhiều hiệu suất. Cả phương pháp và tác vụ đều thực hiện cùng một hoạt động chính xác:

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

Bây giờ tôi cần một MonoBehaviourcái mà tôi có thể sử dụng để kiểm tra tác động của hiệu suất đối với tác vụ và phương pháp. Để tôi có thể thấy tác động tốt hơn đến hiệu suất, tôi sẽ giả vờ rằng tôi muốn chạy điều này trên mười đối tượng trò chơi khác nhau. Tôi cũng sẽ theo dõi khoảng thời gian mà Updatephương thức cần để chạy.

Trong Update, tôi nhận được thời gian bắt đầu. Nếu tôi đang thử nghiệm phương pháp này, tôi lặp qua tất cả các đối tượng trò chơi được mô phỏng và gọi phương pháp tăng cường hiệu suất. Nếu tôi đang thử nghiệm tác vụ, tôi sẽ tạo một Taskvòng lặp mảng mới thông qua tất cả các đối tượng trò chơi được mô phỏng và thêm nhiệm vụ chuyên sâu về hiệu suất vào mảng tác vụ. Sau đó tôi awaitcho tất cả các nhiệm vụ để hoàn thành. Bên ngoài kiểm tra loại phương thức, tôi cập nhật thời gian phương thức, chuyển đổi nó thành ms. Tôi cũng ghi lại nó.

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

Phương pháp chuyên sâu mất khoảng 65 mili giây để hoàn thành trò chơi chạy ở khoảng 12 FPS.

Phương pháp chuyên sâuPhương pháp chuyên sâuPhương pháp chuyên sâu

Nhiệm vụ chuyên sâu mất khoảng 4ms để hoàn thành với trò chơi chạy ở khoảng 200 FPS.

Nhiệm vụ chuyên sâu

Hãy thử điều này với hàng nghìn kẻ thù:

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

Hiển thị và di chuyển một nghìn kẻ thù với phương pháp này mất khoảng 150ms với tốc độ khung hình khoảng 7 FPS.

Ngàn kẻ thù

Hiển thị và di chuyển hàng nghìn kẻ thù trong một nhiệm vụ mất khoảng 50ms với tốc độ khung hình khoảng 30 FPS.

Hiển thị kẻ thù di chuyển

Tại sao không useTasks?

Các tác vụ cực kỳ thành thạo và giảm bớt căng thẳng về hiệu suất trên hệ thống của bạn. Bạn thậm chí có thể sử dụng chúng trong nhiều chủ đề bằng cách sử dụng Thư viện song song tác vụ (TPL).

Tuy nhiên, có một số hạn chế khi sử dụng chúng trong Unity. Hạn chế lớn khi sử dụng Tasktrong Unity là tất cả chúng đều chạy trên Mainluồng. Có, chúng tôi có thể làm cho chúng chạy trên các luồng khác, nhưng Unity đã thực hiện quản lý luồng và bộ nhớ của riêng mình, và bạn có thể tạo lỗi bằng cách tạo nhiều luồng hơn Lõi CPU, điều này gây ra sự cạnh tranh về tài nguyên.

Các tác vụ cũng có thể khó thực hiện chính xác và gỡ lỗi. Khi viết mã ban đầu, tôi kết thúc với tất cả các nhiệm vụ đang chạy, nhưng không có kẻ thù nào di chuyển trên màn hình. Kết quả là tôi cần trả lại cái Task[]mà tôi đã tạo trong Task.

Tác vụ tạo ra nhiều rác ảnh hưởng đến hiệu suất. Chúng cũng không hiển thị trong hồ sơ, vì vậy nếu bạn có một cái đang ảnh hưởng đến hiệu suất, thật khó để theo dõi. Ngoài ra, tôi nhận thấy rằng đôi khi các tác vụ và chức năng cập nhật của tôi tiếp tục chạy từ các cảnh khác.

Các quy trình thống nhất

Theo Unity , “Một quy trình đăng ký là một hàm có thể tạm dừng việc thực thi của nó (lợi nhuận) cho đến khi kết thúc YieldInstruction đã cho .”

Điều này có nghĩa là chúng ta có thể chạy mã và đợi một tác vụ hoàn thành trước khi tiếp tục. Điều này giống như một phương pháp không đồng bộ. Nó sử dụng kiểu trả về IEnumeratorvà chúng tôi yield returnthay vì await.

Unity có một số loại hướng dẫn lợi nhuận khác nhau mà chúng tôi có thể sử dụng, chẳng hạn như WaitForSeconds, hoặc .WaitForEndOfFrameWaitUntilWaitWhile

Để bắt đầu coroutines, chúng ta cần một MonoBehaviourvà sử dụng MonoBehaviour.StartCoroutine.

Để dừng một quy trình đăng ký trước khi hoàn tất, chúng tôi sử dụng MonoBehaviour.StopCoroutine. Khi dừng coroutines, hãy đảm bảo rằng bạn sử dụng cùng một phương pháp như bạn đã sử dụng để bắt đầu nó.

Các trường hợp sử dụng phổ biến cho coroutines trong Unity là đợi tải nội dung và tạo bộ định thời gian hồi chiêu.

Ví dụ: Trình tải cảnh sử dụng chương trình điều tra

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

Kiểm tra tác động của quy trình đăng ký đối với hiệu suất

Hãy xem việc sử dụng một quy trình điều tra tác động như thế nào đến hiệu suất của dự án của chúng tôi. Tôi sẽ chỉ làm điều này với phương pháp chuyên sâu về hiệu suất.

Tôi đã thêm vào Coroutineenum MethodTypevà các biến để theo dõi trạng thái của nó:

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

   ...

   private Coroutine m_performanceCoroutine;

Tôi đã tạo quy trình đăng quang. Điều này tương tự với tác vụ và phương thức chuyên sâu về hiệu suất mà chúng tôi đã tạo trước đó với mã được bổ sung để cập nhật thời gian của phương thức:

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

Trong Updatephương thức này, tôi đã thêm dấu kiểm cho quy trình đăng quang. Tôi cũng đã sửa đổi thời gian phương thức, cập nhật mã và thêm mã để dừng chương trình đăng quang nếu nó đang chạy và chúng tôi đã thay đổi loại phương thức:

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

Quá trình đăng quang chuyên sâu mất khoảng 6 mili giây để hoàn thành khi trò chơi chạy ở khoảng 90 FPS.

Chương trình điều trị chuyên sâu

Hệ thống công việc C # và trình biên dịch liên tục

Hệ thống công việc C # là gì?

Hệ thống công việc C # là việc Unity thực hiện các tác vụ dễ viết, không tạo ra rác mà các tác vụ thực hiện và sử dụng các chuỗi công nhân mà Unity đã tạo. Điều này khắc phục tất cả các nhược điểm của nhiệm vụ.

Unity so sánh các công việc như các chủ đề, nhưng họ nói rằng một công việc thực hiện một nhiệm vụ cụ thể. Các công việc cũng có thể phụ thuộc vào các công việc khác để hoàn thành trước khi chạy; điều này khắc phục sự cố với nhiệm vụ mà tôi đã không di chuyển đúng cách của tôi Unitsvì nó phụ thuộc vào một nhiệm vụ khác để hoàn thành trước.

Các phụ thuộc công việc sẽ được Unity tự động giải quyết cho chúng tôi. Hệ thống công việc cũng có một hệ thống an toàn được tích hợp chủ yếu để bảo vệ khỏi các điều kiện của cuộc đua . Một lưu ý với các công việc là chúng chỉ có thể chứa các biến thành viên là kiểu blittable hoặc kiểu NativeContainer ; đây là một nhược điểm của hệ thống an toàn.

Để sử dụng hệ thống công việc, bạn tạo công việc, lên lịch công việc, đợi công việc hoàn thành, sau đó sử dụng dữ liệu trả về của công việc. Hệ thống công việc là cần thiết để sử dụng Ngăn xếp Công nghệ Định hướng Dữ liệu (DOTS) của Unity.

Để biết thêm chi tiết về hệ thống công việc, hãy xem tài liệu Unity .

Tạo một công việc

Để tạo một công việc, bạn tạo một công stuctviệc triển khai một trong các IJobgiao diện ( IJob , IJobFor , IJobParallelFor , Unity.Engine.Jobs.IJobParallelForTransform ). IJoblà một công việc cơ bản. IJobForIJobForParallelđược sử dụng để thực hiện cùng một thao tác trên mỗi phần tử của vùng chứa gốc hoặc cho một số lần lặp. Sự khác biệt giữa chúng là IJobFor chạy trên một luồng duy nhất, nơi IJobForParallelsẽ được chia thành nhiều luồng.

Tôi sẽ sử dụng IJobđể tạo ra một công việc hoạt động chuyên sâu IJobForIJobForParallelđể tạo ra một công việc có thể di chuyển nhiều kẻ thù xung quanh; điều này chỉ để chúng ta có thể thấy các tác động khác nhau đến hiệu suất. Các công việc này sẽ giống với các tác vụ và phương pháp mà chúng tôi đã tạo trước đó:

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

Thêm các biến thành viên. Trong trường hợp của tôi, tôi IJobkhông cần bất kỳ. Và cần một IJobForphao IJobParallelForcho thời gian đồng bằng hiện tại vì các công việc không có khái niệm về khung; họ hoạt động bên ngoài Unity's MonoBehaviour. Họ cũng cần một mảng float3cho vị trí và một mảng cho tốc độ di chuyển trên trục 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;
}

Bước cuối cùng là thực hiện Executephương thức được yêu cầu. Cả IJobForhai IJobForParallelđều yêu cầu một intchỉ mục của lần lặp hiện tại mà công việc đang thực thi.

Sự khác biệt là thay vì truy cập kẻ thù transformvà di chuyển, chúng tôi sử dụng mảng có trong công việc:

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

Lên lịch công việc

Đầu tiên, chúng ta cần cài đặt công việc và điền dữ liệu công việc:

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

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

Sau đó, chúng tôi lên lịch công việc với JobHandle jobHandle = jobData.Schedule();. Phương Schedulethức trả về một JobHandlecó thể được sử dụng sau này.

Chúng ta không thể sắp xếp một công việc từ bên trong một công việc. Tuy nhiên, chúng tôi có thể tạo công việc mới và điền dữ liệu của họ từ bên trong công việc. Một khi công việc đã được lên lịch, nó không thể bị gián đoạn.

Công việc đòi hỏi hiệu suất cao

Tôi đã tạo ra một phương pháp tạo một công việc mới và lên lịch cho nó. Nó trả về công việc xử lý mà tôi có thể sử dụng trong updatephương thức của mình:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   ....

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

Tôi đã thêm công việc vào enum của mình. Sau đó, trong Updatephương thức, tôi thêm phần casevào switchphần. Tôi đã tạo một mảng JobHandles. Sau đó, tôi lặp qua tất cả các đối tượng trò chơi được mô phỏng, thêm một công việc đã lên lịch cho từng đối tượng vào mảng:

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

       ...
   }
}

MoveEnemy_MoveEnemyParallelJob

Tiếp theo, tôi đã thêm các công việc vào enum của mình. Sau đó, trong Updatephương thức, tôi gọi một MoveEnemyJobphương thức mới, vượt qua thời gian delta. Thông thường, bạn sẽ sử JobFordụng 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;
       }

       ...
   }

   ...

Điều đầu tiên tôi làm là thiết lập một mảng cho các vị trí và một mảng cho vị trí moveYmà tôi sẽ chuyển cho các công việc. Sau đó, tôi điền vào các mảng này với dữ liệu từ kẻ thù. Tiếp theo, tôi tạo công việc và thiết lập dữ liệu của công việc tùy thuộc vào công việc mà tôi muốn sử dụng. Sau đó, tôi lên lịch công việc tùy thuộc vào công việc mà tôi muốn sử dụng và loại lập lịch mà tôi muốn thực hiện:

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

           }
   }

Lấy lại dữ liệu từ một công việc

Chúng ta phải đợi cho công việc được hoàn thành. Chúng ta có thể lấy trạng thái từ trạng thái JobHandlemà chúng ta đã sử dụng khi lên lịch để hoàn thành công việc. Thao tác này sẽ đợi công việc hoàn tất trước khi tiếp tục thực hiện:> handle.Complete();hoặc JobHandle.CompleteAll(jobHandles). Sau khi công việc hoàn tất, công việc NativeContainermà chúng tôi đã sử dụng để thiết lập công việc sẽ có tất cả dữ liệu mà chúng tôi cần sử dụng. Khi chúng tôi lấy dữ liệu từ chúng, chúng tôi phải xử lý chúng đúng cách.

Công việc đòi hỏi hiệu suất cao

Điều này khá đơn giản vì tôi không đọc hoặc ghi bất kỳ dữ liệu nào cho công việc. Tôi đợi cho tất cả các công việc đã được lên lịch hoàn thành sau đó xử lý Nativemảng:

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

       ...
   }
}

Công việc chuyên sâu mất khoảng 6ms để hoàn thành với trò chơi chạy ở khoảng 90 FPS.

Công việc chuyên sâu

Công MoveEnemyviệc

Tôi thêm các séc hoàn chỉnh thích hợp:

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

Sau khi kiểm tra loại phương pháp, tôi lặp qua tất cả kẻ thù, thiết lập transformvị trí của chúng và moveYđến dữ liệu đã được thiết lập trong công việc. Tiếp theo, tôi xử lý đúng cách các mảng gốc:

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

Việc hiển thị và di chuyển hàng nghìn kẻ thù với công việc mất khoảng 160ms với tốc độ khung hình khoảng 7 FPS mà không tăng hiệu suất.

Không tăng hiệu suất

Hiển thị và di chuyển hàng nghìn kẻ thù song song với công việc mất khoảng 30ms với tốc độ khung hình khoảng 30 FPS.

Công việc song song

Trình biên dịch bùng nổ trong Unity là gì?

Trình biên dịch liên tục là trình biên dịch chuyển từ mã bytecode sang mã gốc. Việc sử dụng tính năng này với Hệ thống công việc C # sẽ cải thiện chất lượng của mã được tạo, giúp bạn tăng hiệu suất đáng kể cũng như giảm mức tiêu thụ pin trên thiết bị di động.

Để sử dụng điều này, bạn chỉ cần nói với Unity rằng bạn muốn sử dụng trình biên dịch liên tục trong công việc với [BurstCompile]thuộc tính:

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

Sau đó, trong Unity, chọn Jobs > Burst> Enable Completion

Cho phép hoàn thành

Burst là Just-In-Time (JIT) khi ở trong Trình chỉnh sửa, có nghĩa là điều này có thể ngừng hoạt động khi ở Chế độ phát. Khi bạn xây dựng dự án của mình, nó là Ahead-Of-Time (AOT), có nghĩa là điều này cần được kích hoạt trước khi xây dựng dự án của bạn. Bạn có thể làm như vậy bằng cách chỉnh sửa phần Cài đặt Burst AOT trong Cửa sổ Cài đặt Dự án .

Cài đặt Burst AOT

Để biết thêm chi tiết về trình biên dịch cụm, hãy xem tài liệu Unity .

Một công việc chuyên sâu về hiệu suất với trình biên dịch liên tục

Một công việc chuyên sâu với liên tục mất khoảng 3ms để hoàn thành với trò chơi chạy ở khoảng 150 FPS.

Công việc chuyên sâu với Burst

Hiển thị và di chuyển hàng nghìn kẻ thù, công việc với liên tục mất khoảng 30ms với tốc độ khung hình khoảng 30 FPS.

Burst 30 mili giây

Hiển thị và di chuyển hàng nghìn kẻ thù, công việc song song với liên tục mất khoảng 6ms với tốc độ khung hình khoảng 80 đến 90 FPS.

6 mili giây

Sự kết luận

Chúng tôi có thể sử dụng Taskđể tăng hiệu suất của các ứng dụng Unity của mình, nhưng có một số hạn chế khi sử dụng chúng. Tốt hơn là sử dụng những thứ được đóng gói trong Unity tùy thuộc vào những gì chúng ta muốn làm. Sử dụng coroutines nếu chúng ta muốn đợi một thứ gì đó kết thúc tải một cách không đồng bộ; chúng tôi có thể bắt đầu quy trình đăng ký và không dừng quá trình chạy chương trình của chúng tôi.

Chúng ta có thể sử dụng Hệ thống công việc C # với trình biên dịch liên tục để đạt được hiệu suất lớn trong khi không phải lo lắng về tất cả nội dung quản lý luồng khi thực hiện các tác vụ đòi hỏi nhiều quy trình. Sử dụng các hệ thống có sẵn, chúng tôi chắc chắn rằng nó được thực hiện một cách an toàn và không gây ra bất kỳ lỗi hoặc lỗi không mong muốn nào.

Các công việc đã chạy tốt hơn một chút so với các công việc không sử dụng trình biên dịch liên tục, nhưng đó là do chi phí bổ sung đằng sau hậu trường ít hơn để thiết lập mọi thứ một cách an toàn cho chúng tôi. Khi sử dụng trình biên dịch bùng nổ, các công việc của chúng tôi đã thực hiện các nhiệm vụ của chúng tôi. Khi bạn cần tất cả hiệu suất bổ sung mà bạn có thể nhận được, hãy sử dụng Hệ thống công việc C # với liên tục.

Các tệp dự án cho việc này có thể được tìm thấy trên GitHub của tôi .

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

#csharp  #async  #await 

What is GEEK

Buddha Community

Async, Await Và Task Là Gì Trong C #
Hong  Nhung

Hong Nhung

1656374400

Async, Await Và Task Là Gì Trong C #

Hiệu suất là tất cả mọi thứ khi bạn đang cố gắng xuất bản lên web, thiết bị di động, bảng điều khiển và thậm chí một số PC cấp thấp hơn. Một trò chơi hoặc ứng dụng chạy ở tốc độ dưới 30 FPS có thể gây ra sự thất vọng cho người dùng. Chúng ta hãy xem xét một số thứ chúng ta có thể sử dụng để tăng hiệu suất bằng cách giảm tải cho CPU.

Trong bài đăng này, chúng tôi sẽ đề cập đến những gì async, awaitTasktrong C # là gì và cách sử dụng chúng trong Unity để đạt được hiệu suất trong dự án của bạn. Tiếp theo, chúng ta sẽ xem xét một số gói có sẵn của Unity: coroutines, C # Job System và trình biên dịch bùng nổ. Chúng tôi sẽ xem xét chúng là gì, cách sử dụng chúng và cách chúng tăng hiệu suất trong dự án của bạn.

Để bắt đầu dự án này, tôi sẽ sử dụng Unity 2021.3.4f1. Tôi chưa thử nghiệm mã này trên bất kỳ phiên bản Unity nào khác; tất cả các khái niệm ở đây sẽ hoạt động trên bất kỳ phiên bản Unity nào sau Unity 2019.3. Kết quả hiệu suất của bạn có thể khác nếu sử dụng phiên bản cũ hơn vì Unity đã thực hiện một số cải tiến đáng kể với mô hình lập trình async / await vào năm 2021. Đọc thêm về nó trong blog Unity và .NET của Unity, phần tiếp theo là phần có nhãn “Hiện đại hóa Thời gian chạy thống nhất. ”

Tôi đã tạo một dự án 2D (URP) Core mới, nhưng bạn có thể sử dụng dự án này trong bất kỳ loại dự án nào bạn thích.

Tôi có một sprite mà tôi nhận được từ Space Shooter (Redux, cùng với phông chữ và âm thanh) của Kenney Vleugels .

Tôi đã tạo một nhà lắp ghép của kẻ thù có chứa Sprite Render và một Thành phần của kẻ thù. Thành phần địch là một thành phần MonoBehaviourcó a Transformvà a floatđể theo dõi vị trí và tốc độ di chuyển trên trục y:

using UnityEngine;

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

Cái gì async, awaitTaskcó trong C #

Là gì async?

Trong C #, các phương thức có thể có một asynctừ khóa phía trước, nghĩa là các phương thức đó là phương thức không đồng bộ. Đây chỉ là một cách để nói với trình biên dịch rằng chúng ta muốn có thể thực thi mã bên trong và cho phép người gọi phương thức đó tiếp tục thực thi trong khi chờ phương thức này kết thúc.

Một ví dụ về điều này sẽ là nấu một bữa ăn. Bạn sẽ bắt đầu nấu thịt, và trong khi thịt đang nấu và bạn chờ nó hoàn thành, bạn sẽ bắt đầu làm các mặt. Trong khi thức ăn đang nấu, bạn sẽ bắt đầu dọn bàn ăn. Một ví dụ về điều này trong mã sẽ là static async Task<Steak> MakeSteak(int number).

Unity cũng có tất cả các loại phương thức có sẵn mà bạn có thể gọi không đồng bộ; xem tài liệu Unity để biết danh sách các phương pháp. Với cách Unity xử lý việc quản lý bộ nhớ, nó sử dụng coroutines hoặcAsyncOperation Hệ thống công việc C # .

Bạn awaitsử dụng nó như thế nào và nó là gì?

Trong C #, bạn có thể đợi hoạt động không đồng bộ hoàn tất bằng cách sử dụng awaittừ khóa. Điều này được sử dụng bên trong bất kỳ phương thức nào có asynctừ khóa để chờ một thao tác tiếp tục:

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

Xem các tài liệu của Microsoft để biết thêm về await.

A là gì Taskvà bạn sử dụng nó như thế nào?

A Tasklà một phương thức không đồng bộ thực hiện một thao tác đơn lẻ và không trả về giá trị. Đối với một Tasktrả về một giá trị, chúng tôi sẽ sử dụng Task<TResult>.

Để sử dụng một tác vụ, chúng ta tạo nó giống như tạo bất kỳ đối tượng mới nào trong C # Task t1 = new Task(void Action):. Tiếp theo, chúng ta bắt đầu nhiệm vụ t1.wait. Cuối cùng, chúng tôi đợi nhiệm vụ hoàn thành với t1.wait.

Có một số cách để tạo, bắt đầu và chạy tác vụ. Task t2 = Task.Run(void Action)sẽ tạo và bắt đầu một nhiệm vụ. await Task.Run(void Action)sẽ tạo, bắt đầu và đợi tác vụ hoàn thành. Chúng tôi có thể sử dụng cách thay thế phổ biến nhất với Task t3 = Task.Factory.Start(void Action).

Có một số cách mà chúng ta có thể đợi Tác vụ hoàn thành. int index = Task.WaitAny(Task[])sẽ đợi bất kỳ nhiệm vụ nào hoàn thành và cung cấp cho chúng tôi chỉ số của nhiệm vụ đã hoàn thành trong mảng. await Task.WaitAll(Task[])sẽ đợi tất cả các nhiệm vụ hoàn thành.

Để biết thêm về các tác vụ, hãy xem Tài liệu Microsoft .

Một taskví dụ đơn giản

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

Nhiệm vụ ảnh hưởng như thế nào đến hiệu suất

Bây giờ chúng ta hãy so sánh hiệu suất của một tác vụ so với hiệu suất của một phương pháp.

Tôi sẽ cần một lớp tĩnh mà tôi có thể sử dụng trong tất cả các lần kiểm tra hiệu suất của mình. Nó sẽ có một phương thức và một nhiệm vụ mô phỏng một hoạt động đòi hỏi nhiều hiệu suất. Cả phương pháp và tác vụ đều thực hiện cùng một hoạt động chính xác:

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

Bây giờ tôi cần một MonoBehaviourcái mà tôi có thể sử dụng để kiểm tra tác động của hiệu suất đối với tác vụ và phương pháp. Để tôi có thể thấy tác động tốt hơn đến hiệu suất, tôi sẽ giả vờ rằng tôi muốn chạy điều này trên mười đối tượng trò chơi khác nhau. Tôi cũng sẽ theo dõi khoảng thời gian mà Updatephương thức cần để chạy.

Trong Update, tôi nhận được thời gian bắt đầu. Nếu tôi đang thử nghiệm phương pháp này, tôi lặp qua tất cả các đối tượng trò chơi được mô phỏng và gọi phương pháp tăng cường hiệu suất. Nếu tôi đang thử nghiệm tác vụ, tôi sẽ tạo một Taskvòng lặp mảng mới thông qua tất cả các đối tượng trò chơi được mô phỏng và thêm nhiệm vụ chuyên sâu về hiệu suất vào mảng tác vụ. Sau đó tôi awaitcho tất cả các nhiệm vụ để hoàn thành. Bên ngoài kiểm tra loại phương thức, tôi cập nhật thời gian phương thức, chuyển đổi nó thành ms. Tôi cũng ghi lại nó.

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

Phương pháp chuyên sâu mất khoảng 65 mili giây để hoàn thành trò chơi chạy ở khoảng 12 FPS.

Phương pháp chuyên sâuPhương pháp chuyên sâuPhương pháp chuyên sâu

Nhiệm vụ chuyên sâu mất khoảng 4ms để hoàn thành với trò chơi chạy ở khoảng 200 FPS.

Nhiệm vụ chuyên sâu

Hãy thử điều này với hàng nghìn kẻ thù:

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

Hiển thị và di chuyển một nghìn kẻ thù với phương pháp này mất khoảng 150ms với tốc độ khung hình khoảng 7 FPS.

Ngàn kẻ thù

Hiển thị và di chuyển hàng nghìn kẻ thù trong một nhiệm vụ mất khoảng 50ms với tốc độ khung hình khoảng 30 FPS.

Hiển thị kẻ thù di chuyển

Tại sao không useTasks?

Các tác vụ cực kỳ thành thạo và giảm bớt căng thẳng về hiệu suất trên hệ thống của bạn. Bạn thậm chí có thể sử dụng chúng trong nhiều chủ đề bằng cách sử dụng Thư viện song song tác vụ (TPL).

Tuy nhiên, có một số hạn chế khi sử dụng chúng trong Unity. Hạn chế lớn khi sử dụng Tasktrong Unity là tất cả chúng đều chạy trên Mainluồng. Có, chúng tôi có thể làm cho chúng chạy trên các luồng khác, nhưng Unity đã thực hiện quản lý luồng và bộ nhớ của riêng mình, và bạn có thể tạo lỗi bằng cách tạo nhiều luồng hơn Lõi CPU, điều này gây ra sự cạnh tranh về tài nguyên.

Các tác vụ cũng có thể khó thực hiện chính xác và gỡ lỗi. Khi viết mã ban đầu, tôi kết thúc với tất cả các nhiệm vụ đang chạy, nhưng không có kẻ thù nào di chuyển trên màn hình. Kết quả là tôi cần trả lại cái Task[]mà tôi đã tạo trong Task.

Tác vụ tạo ra nhiều rác ảnh hưởng đến hiệu suất. Chúng cũng không hiển thị trong hồ sơ, vì vậy nếu bạn có một cái đang ảnh hưởng đến hiệu suất, thật khó để theo dõi. Ngoài ra, tôi nhận thấy rằng đôi khi các tác vụ và chức năng cập nhật của tôi tiếp tục chạy từ các cảnh khác.

Các quy trình thống nhất

Theo Unity , “Một quy trình đăng ký là một hàm có thể tạm dừng việc thực thi của nó (lợi nhuận) cho đến khi kết thúc YieldInstruction đã cho .”

Điều này có nghĩa là chúng ta có thể chạy mã và đợi một tác vụ hoàn thành trước khi tiếp tục. Điều này giống như một phương pháp không đồng bộ. Nó sử dụng kiểu trả về IEnumeratorvà chúng tôi yield returnthay vì await.

Unity có một số loại hướng dẫn lợi nhuận khác nhau mà chúng tôi có thể sử dụng, chẳng hạn như WaitForSeconds, hoặc .WaitForEndOfFrameWaitUntilWaitWhile

Để bắt đầu coroutines, chúng ta cần một MonoBehaviourvà sử dụng MonoBehaviour.StartCoroutine.

Để dừng một quy trình đăng ký trước khi hoàn tất, chúng tôi sử dụng MonoBehaviour.StopCoroutine. Khi dừng coroutines, hãy đảm bảo rằng bạn sử dụng cùng một phương pháp như bạn đã sử dụng để bắt đầu nó.

Các trường hợp sử dụng phổ biến cho coroutines trong Unity là đợi tải nội dung và tạo bộ định thời gian hồi chiêu.

Ví dụ: Trình tải cảnh sử dụng chương trình điều tra

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

Kiểm tra tác động của quy trình đăng ký đối với hiệu suất

Hãy xem việc sử dụng một quy trình điều tra tác động như thế nào đến hiệu suất của dự án của chúng tôi. Tôi sẽ chỉ làm điều này với phương pháp chuyên sâu về hiệu suất.

Tôi đã thêm vào Coroutineenum MethodTypevà các biến để theo dõi trạng thái của nó:

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

   ...

   private Coroutine m_performanceCoroutine;

Tôi đã tạo quy trình đăng quang. Điều này tương tự với tác vụ và phương thức chuyên sâu về hiệu suất mà chúng tôi đã tạo trước đó với mã được bổ sung để cập nhật thời gian của phương thức:

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

Trong Updatephương thức này, tôi đã thêm dấu kiểm cho quy trình đăng quang. Tôi cũng đã sửa đổi thời gian phương thức, cập nhật mã và thêm mã để dừng chương trình đăng quang nếu nó đang chạy và chúng tôi đã thay đổi loại phương thức:

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

Quá trình đăng quang chuyên sâu mất khoảng 6 mili giây để hoàn thành khi trò chơi chạy ở khoảng 90 FPS.

Chương trình điều trị chuyên sâu

Hệ thống công việc C # và trình biên dịch liên tục

Hệ thống công việc C # là gì?

Hệ thống công việc C # là việc Unity thực hiện các tác vụ dễ viết, không tạo ra rác mà các tác vụ thực hiện và sử dụng các chuỗi công nhân mà Unity đã tạo. Điều này khắc phục tất cả các nhược điểm của nhiệm vụ.

Unity so sánh các công việc như các chủ đề, nhưng họ nói rằng một công việc thực hiện một nhiệm vụ cụ thể. Các công việc cũng có thể phụ thuộc vào các công việc khác để hoàn thành trước khi chạy; điều này khắc phục sự cố với nhiệm vụ mà tôi đã không di chuyển đúng cách của tôi Unitsvì nó phụ thuộc vào một nhiệm vụ khác để hoàn thành trước.

Các phụ thuộc công việc sẽ được Unity tự động giải quyết cho chúng tôi. Hệ thống công việc cũng có một hệ thống an toàn được tích hợp chủ yếu để bảo vệ khỏi các điều kiện của cuộc đua . Một lưu ý với các công việc là chúng chỉ có thể chứa các biến thành viên là kiểu blittable hoặc kiểu NativeContainer ; đây là một nhược điểm của hệ thống an toàn.

Để sử dụng hệ thống công việc, bạn tạo công việc, lên lịch công việc, đợi công việc hoàn thành, sau đó sử dụng dữ liệu trả về của công việc. Hệ thống công việc là cần thiết để sử dụng Ngăn xếp Công nghệ Định hướng Dữ liệu (DOTS) của Unity.

Để biết thêm chi tiết về hệ thống công việc, hãy xem tài liệu Unity .

Tạo một công việc

Để tạo một công việc, bạn tạo một công stuctviệc triển khai một trong các IJobgiao diện ( IJob , IJobFor , IJobParallelFor , Unity.Engine.Jobs.IJobParallelForTransform ). IJoblà một công việc cơ bản. IJobForIJobForParallelđược sử dụng để thực hiện cùng một thao tác trên mỗi phần tử của vùng chứa gốc hoặc cho một số lần lặp. Sự khác biệt giữa chúng là IJobFor chạy trên một luồng duy nhất, nơi IJobForParallelsẽ được chia thành nhiều luồng.

Tôi sẽ sử dụng IJobđể tạo ra một công việc hoạt động chuyên sâu IJobForIJobForParallelđể tạo ra một công việc có thể di chuyển nhiều kẻ thù xung quanh; điều này chỉ để chúng ta có thể thấy các tác động khác nhau đến hiệu suất. Các công việc này sẽ giống với các tác vụ và phương pháp mà chúng tôi đã tạo trước đó:

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

Thêm các biến thành viên. Trong trường hợp của tôi, tôi IJobkhông cần bất kỳ. Và cần một IJobForphao IJobParallelForcho thời gian đồng bằng hiện tại vì các công việc không có khái niệm về khung; họ hoạt động bên ngoài Unity's MonoBehaviour. Họ cũng cần một mảng float3cho vị trí và một mảng cho tốc độ di chuyển trên trục 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;
}

Bước cuối cùng là thực hiện Executephương thức được yêu cầu. Cả IJobForhai IJobForParallelđều yêu cầu một intchỉ mục của lần lặp hiện tại mà công việc đang thực thi.

Sự khác biệt là thay vì truy cập kẻ thù transformvà di chuyển, chúng tôi sử dụng mảng có trong công việc:

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

Lên lịch công việc

Đầu tiên, chúng ta cần cài đặt công việc và điền dữ liệu công việc:

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

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

Sau đó, chúng tôi lên lịch công việc với JobHandle jobHandle = jobData.Schedule();. Phương Schedulethức trả về một JobHandlecó thể được sử dụng sau này.

Chúng ta không thể sắp xếp một công việc từ bên trong một công việc. Tuy nhiên, chúng tôi có thể tạo công việc mới và điền dữ liệu của họ từ bên trong công việc. Một khi công việc đã được lên lịch, nó không thể bị gián đoạn.

Công việc đòi hỏi hiệu suất cao

Tôi đã tạo ra một phương pháp tạo một công việc mới và lên lịch cho nó. Nó trả về công việc xử lý mà tôi có thể sử dụng trong updatephương thức của mình:

public class PerformanceTaskCoroutineJob : MonoBehaviour
{
   ....

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

Tôi đã thêm công việc vào enum của mình. Sau đó, trong Updatephương thức, tôi thêm phần casevào switchphần. Tôi đã tạo một mảng JobHandles. Sau đó, tôi lặp qua tất cả các đối tượng trò chơi được mô phỏng, thêm một công việc đã lên lịch cho từng đối tượng vào mảng:

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

       ...
   }
}

MoveEnemy_MoveEnemyParallelJob

Tiếp theo, tôi đã thêm các công việc vào enum của mình. Sau đó, trong Updatephương thức, tôi gọi một MoveEnemyJobphương thức mới, vượt qua thời gian delta. Thông thường, bạn sẽ sử JobFordụng 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;
       }

       ...
   }

   ...

Điều đầu tiên tôi làm là thiết lập một mảng cho các vị trí và một mảng cho vị trí moveYmà tôi sẽ chuyển cho các công việc. Sau đó, tôi điền vào các mảng này với dữ liệu từ kẻ thù. Tiếp theo, tôi tạo công việc và thiết lập dữ liệu của công việc tùy thuộc vào công việc mà tôi muốn sử dụng. Sau đó, tôi lên lịch công việc tùy thuộc vào công việc mà tôi muốn sử dụng và loại lập lịch mà tôi muốn thực hiện:

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

           }
   }

Lấy lại dữ liệu từ một công việc

Chúng ta phải đợi cho công việc được hoàn thành. Chúng ta có thể lấy trạng thái từ trạng thái JobHandlemà chúng ta đã sử dụng khi lên lịch để hoàn thành công việc. Thao tác này sẽ đợi công việc hoàn tất trước khi tiếp tục thực hiện:> handle.Complete();hoặc JobHandle.CompleteAll(jobHandles). Sau khi công việc hoàn tất, công việc NativeContainermà chúng tôi đã sử dụng để thiết lập công việc sẽ có tất cả dữ liệu mà chúng tôi cần sử dụng. Khi chúng tôi lấy dữ liệu từ chúng, chúng tôi phải xử lý chúng đúng cách.

Công việc đòi hỏi hiệu suất cao

Điều này khá đơn giản vì tôi không đọc hoặc ghi bất kỳ dữ liệu nào cho công việc. Tôi đợi cho tất cả các công việc đã được lên lịch hoàn thành sau đó xử lý Nativemảng:

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

       ...
   }
}

Công việc chuyên sâu mất khoảng 6ms để hoàn thành với trò chơi chạy ở khoảng 90 FPS.

Công việc chuyên sâu

Công MoveEnemyviệc

Tôi thêm các séc hoàn chỉnh thích hợp:

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

Sau khi kiểm tra loại phương pháp, tôi lặp qua tất cả kẻ thù, thiết lập transformvị trí của chúng và moveYđến dữ liệu đã được thiết lập trong công việc. Tiếp theo, tôi xử lý đúng cách các mảng gốc:

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

Việc hiển thị và di chuyển hàng nghìn kẻ thù với công việc mất khoảng 160ms với tốc độ khung hình khoảng 7 FPS mà không tăng hiệu suất.

Không tăng hiệu suất

Hiển thị và di chuyển hàng nghìn kẻ thù song song với công việc mất khoảng 30ms với tốc độ khung hình khoảng 30 FPS.

Công việc song song

Trình biên dịch bùng nổ trong Unity là gì?

Trình biên dịch liên tục là trình biên dịch chuyển từ mã bytecode sang mã gốc. Việc sử dụng tính năng này với Hệ thống công việc C # sẽ cải thiện chất lượng của mã được tạo, giúp bạn tăng hiệu suất đáng kể cũng như giảm mức tiêu thụ pin trên thiết bị di động.

Để sử dụng điều này, bạn chỉ cần nói với Unity rằng bạn muốn sử dụng trình biên dịch liên tục trong công việc với [BurstCompile]thuộc tính:

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

Sau đó, trong Unity, chọn Jobs > Burst> Enable Completion

Cho phép hoàn thành

Burst là Just-In-Time (JIT) khi ở trong Trình chỉnh sửa, có nghĩa là điều này có thể ngừng hoạt động khi ở Chế độ phát. Khi bạn xây dựng dự án của mình, nó là Ahead-Of-Time (AOT), có nghĩa là điều này cần được kích hoạt trước khi xây dựng dự án của bạn. Bạn có thể làm như vậy bằng cách chỉnh sửa phần Cài đặt Burst AOT trong Cửa sổ Cài đặt Dự án .

Cài đặt Burst AOT

Để biết thêm chi tiết về trình biên dịch cụm, hãy xem tài liệu Unity .

Một công việc chuyên sâu về hiệu suất với trình biên dịch liên tục

Một công việc chuyên sâu với liên tục mất khoảng 3ms để hoàn thành với trò chơi chạy ở khoảng 150 FPS.

Công việc chuyên sâu với Burst

Hiển thị và di chuyển hàng nghìn kẻ thù, công việc với liên tục mất khoảng 30ms với tốc độ khung hình khoảng 30 FPS.

Burst 30 mili giây

Hiển thị và di chuyển hàng nghìn kẻ thù, công việc song song với liên tục mất khoảng 6ms với tốc độ khung hình khoảng 80 đến 90 FPS.

6 mili giây

Sự kết luận

Chúng tôi có thể sử dụng Taskđể tăng hiệu suất của các ứng dụng Unity của mình, nhưng có một số hạn chế khi sử dụng chúng. Tốt hơn là sử dụng những thứ được đóng gói trong Unity tùy thuộc vào những gì chúng ta muốn làm. Sử dụng coroutines nếu chúng ta muốn đợi một thứ gì đó kết thúc tải một cách không đồng bộ; chúng tôi có thể bắt đầu quy trình đăng ký và không dừng quá trình chạy chương trình của chúng tôi.

Chúng ta có thể sử dụng Hệ thống công việc C # với trình biên dịch liên tục để đạt được hiệu suất lớn trong khi không phải lo lắng về tất cả nội dung quản lý luồng khi thực hiện các tác vụ đòi hỏi nhiều quy trình. Sử dụng các hệ thống có sẵn, chúng tôi chắc chắn rằng nó được thực hiện một cách an toàn và không gây ra bất kỳ lỗi hoặc lỗi không mong muốn nào.

Các công việc đã chạy tốt hơn một chút so với các công việc không sử dụng trình biên dịch liên tục, nhưng đó là do chi phí bổ sung đằng sau hậu trường ít hơn để thiết lập mọi thứ một cách an toàn cho chúng tôi. Khi sử dụng trình biên dịch bùng nổ, các công việc của chúng tôi đã thực hiện các nhiệm vụ của chúng tôi. Khi bạn cần tất cả hiệu suất bổ sung mà bạn có thể nhận được, hãy sử dụng Hệ thống công việc C # với liên tục.

Các tệp dự án cho việc này có thể được tìm thấy trên GitHub của tôi .

Nguồn: 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

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