02/04/2025 - 05:19 · 10 Min read

ET Framework 1: ETTask Asynchronous programming

ETTask là nền tảng cho lập trình bất đồng bộ trong framework ET. Nó cung cấp một giải pháp thay thế hiệu năng cao cho System.Threading.Tasks.Task của .NET

ET Framework 1: ETTask Asynchronous programming

Dạo này đang nghiên cứu ET Framework của các pháp sư Trung Hoa :)) Post docs đây cho dễ đọc

1. Giới thiệu (Introduction)

ETTask là nền tảng cho lập trình bất đồng bộ trong framework ET. Nó cung cấp một giải pháp thay thế hiệu năng cao cho System.Threading.Tasks.Task của .NET, được tối ưu hóa đặc biệt cho môi trường game server đơn luồng (single-threaded) hoặc mô hình Actor của ET.

Mục đích chính:

  • Cung cấp cơ chế async/await quen thuộc trong C#.

  • Tối ưu hóa hiệu năng và giảm cấp phát bộ nhớ (GC pressure) thông qua object pooling.

  • Cung cấp sự kiểm soát chặt chẽ hơn đối với việc thực thi các tác vụ bất đồng bộ.

Tính năng cốt lõi:

  • Hỗ trợ đầy đủ async/await.

  • Các phiên bản: ETTask (không trả về giá trị), ETTask<T> (trả về giá trị kiểu T), ETVoid (fire-and-forget), ETTaskCompleted (đại diện cho tác vụ đã hoàn thành).

  • Tích hợp cơ chế Object Pooling để tái sử dụng các đối tượng ETTask.

  • Cung cấp các phương thức trợ giúp (ETTaskHelper) để xử lý nhiều tác vụ (WaitAll, WaitAny).

  • Hỗ trợ hủy tác vụ thông qua ETCancellationToken.

2. Bản chất & Nguyên lý hoạt động (Underlying Concepts & Principles)

  • Tại sao không dùng System.Threading.Tasks.Task? Task của .NET được thiết kế cho nhiều kịch bản đa luồng phức tạp, đi kèm với SynchronizationContext và các cơ chế lập lịch (scheduling) có thể gây ra overhead không cần thiết trong môi trường server ET, nơi thường hoạt động theo mô hình đơn luồng trên mỗi Actor/Thread. ETTask được thiết kế gọn nhẹ hơn, tập trung vào hiệu năng và giảm thiểu rác thải bộ nhớ.

  • async/awaitAsyncMethodBuilder: Khi bạn viết một phương thức async ETTask hoặc async ETTask<T>, trình biên dịch C# sẽ biến đổi nó thành một máy trạng thái (state machine). Các struct như ETAsyncTaskMethodBuilder, AsyncETVoidMethodBuilder, AsyncETTaskCompletedMethodBuilder đóng vai trò cầu nối giữa máy trạng thái này và ETTask. Chúng định nghĩa cách máy trạng thái được tạo (Create), bắt đầu (Start), xử lý khi một await hoàn thành (AwaitOnCompleted, AwaitUnsafeOnCompleted), và cách thiết lập kết quả (SetResult) hoặc lỗi (SetException).

  • Object Pooling: Để giảm gánh nặng cho Garbage Collector (GC), ETTaskETTask<T> có thể được tái sử dụng. Khi một ETTask được tạo với fromPool = true và hoàn thành (thông qua GetResult), nó sẽ được đưa trở lại một hàng đợi (queue) để tái sử dụng sau này (Recycle). Điều này đòi hỏi sự cẩn thận từ người lập trình (xem phần Phân tích Chuyên sâu).

  • Mô hình thực thi: Trong ET, các continuation (phần mã sau await) của ETTask thường được thực thi trên cùng một luồng logic (ET an toàn luồng trong một Fiber/Actor) nơi await được gọi, đảm bảo tính nhất quán của trạng thái mà không cần các cơ chế khóa phức tạp.

3. Hướng dẫn sử dụng (Usage Guide)

  • Viết hàm bất đồng bộ:

    • Sử dụng async ETTask cho các hàm không trả về giá trị nhưng cần được await.

    • Sử dụng async ETTask<T> cho các hàm trả về giá trị kiểu T.

    • Sử dụng async ETVoid cho các hàm "fire-and-forget" (không cần await và không quan tâm kết quả/lỗi - thận trọng khi sử dụng).

  • Tạo ETTask thủ công (ít dùng hơn):

    • ETTask.Create(bool fromPool = false): Tạo một ETTask mới.

    • ETTask<T>.Create(bool fromPool = false): Tạo một ETTask<T> mới.

    • Sử dụng SetResult() hoặc SetResult(T result) để đánh dấu hoàn thành thành công.

    • Sử dụng SetException(Exception e) để đánh dấu hoàn thành với lỗi.

  • Await một ETTask: Dùng từ khóa await như với Task thông thường.

    async ETTask MyAsyncFunction()
    {
        Log.Debug("Bắt đầu chờ...");
        await ETTask.Delay(1000); // Ví dụ chờ 1 giây
        Log.Debug("Chờ xong!");
    
        int result = await GetSomeValueAsync();
        Log.Debug($"Giá trị nhận được: {result}");
    }
    
    async ETTask<int> GetSomeValueAsync()
    {
        await ETTask.Delay(500);
        return 123;
    }
    
  • Hủy tác vụ (Cancellation):

    • Tạo ETCancellationToken.

    • Truyền token vào các hàm bất đồng bộ có hỗ trợ.

    • Gọi token.Cancel() để yêu cầu hủy.

    • Kiểm tra token.IsCancel() bên trong hàm bất đồng bộ.

  • Chờ nhiều tác vụ:

    • ETTaskHelper.WaitAll(tasks): Chờ tất cả các task trong danh sách/mảng hoàn thành.

    • ETTaskHelper.WaitAny(tasks): Chờ bất kỳ task nào trong danh sách/mảng hoàn thành.

4. Ví dụ Mã nguồn (Code Examples)

Ví dụ 1: Hàm async cơ bản

using ET;
using System; // For Exception

public async ETTask LoadGameDataAsync()
{
    Log.Info("Bắt đầu tải dữ liệu tài nguyên...");
    // Giả sử ResourceComponent.LoadAsync là một hàm async ETTask
    await ResourceComponent.Instance.LoadAsync("MainAssetBundle");
    Log.Info("Tải dữ liệu tài nguyên xong.");

    try
    {
        Log.Info("Bắt đầu tải cấu hình người chơi...");
        var config = await LoadPlayerConfigAsync(1001); // Giả sử trả về ETTask<PlayerConfig>
        Log.Info($"Tải cấu hình cho người chơi {config.PlayerId} thành công.");
    }
    catch (Exception e)
    {
        Log.Error($"Lỗi khi tải cấu hình người chơi: {e}");
    }
}

public async ETTask<PlayerConfig> LoadPlayerConfigAsync(long playerId)
{
    // Giả lập việc tải từ DB hoặc mạng
    await TimerComponent.Instance.WaitAsync(200); // Chờ 200ms
    if (playerId == 0)
    {
        throw new ArgumentException("Player ID không hợp lệ");
    }
    return new PlayerConfig { PlayerId = playerId, Name = $"Player_{playerId}" };
}

public class PlayerConfig { public long PlayerId; public string Name; }

// Để chạy ví dụ này (trong một ngữ cảnh ET phù hợp)
// LoadGameDataAsync().Coroutine(); // Gọi Coroutine() để bắt đầu thực thi mà không cần await ngay lập tức

Ví dụ 2: Sử dụng WaitAll

using ET;
using System.Collections.Generic;

public async ETTask LoadAllRequiredAssets()
{
    Log.Info("Bắt đầu tải các asset cần thiết song song...");
    List<ETTask> loadingTasks = new List<ETTask>();

    // Giả sử AssetLoader.LoadAsync trả về ETTask
    loadingTasks.Add(AssetLoader.LoadAsync("UI/Common"));
    loadingTasks.Add(AssetLoader.LoadAsync("Characters/Hero"));
    loadingTasks.Add(AssetLoader.LoadAsync("Scenes/Main"));

    // Chờ tất cả các task tải hoàn thành
    await ETTaskHelper.WaitAll(loadingTasks);

    Log.Info("Tất cả asset cần thiết đã được tải.");
}

public static class AssetLoader
{
    public static async ETTask LoadAsync(string path)
    {
        Log.Debug($"Đang tải asset: {path}");
        int delay = path.Length * 100; // Giả lập thời gian tải khác nhau
        await TimerComponent.Instance.WaitAsync(delay);
        Log.Debug($"Đã tải xong: {path}");
    }
}

// Gọi: LoadAllRequiredAssets().Coroutine();

Ví dụ 3: Sử dụng Object Pooling (Cẩn thận!)

using ET;

public async ETTask ProcessWithPooledTask()
{
    ETTask<int> tcs = null;
    try
    {
        // Lấy task từ pool
        tcs = ETTask<int>.Create(true); // fromPool = true

        // Thực hiện một công việc bất đồng bộ nào đó và đặt kết quả cho tcs
        StartBackgroundWork(tcs); // Hàm này sẽ gọi tcs.SetResult(value) hoặc tcs.SetException(e)

        Log.Debug("Đang chờ kết quả từ task trong pool...");
        int result = await tcs; // Chờ task hoàn thành
        // QUAN TRỌNG: Sau khi await, tcs có thể đã bị Recycle và trả về pool.
        // KHÔNG ĐƯỢC sử dụng lại biến 'tcs' ở đây cho các mục đích khác liên quan đến task vừa await.
        // Việc GetResult() (ẩn sau await) đã xử lý việc Recycle nếu task thành công.

        Log.Debug($"Kết quả từ task trong pool: {result}");
    }
    catch (Exception e)
    {
        Log.Error($"Lỗi xảy ra với pooled task: {e}");
        // Lưu ý: Nếu lỗi xảy ra và GetResult() được gọi (ẩn sau await),
        // ExceptionDispatchInfo sẽ được ném ra và task cũng được Recycle.
    }
    // Không cần và không nên gọi tcs.SetResult() hay Recycle() ở đây nữa.
}

// Hàm giả lập công việc nền
private async void StartBackgroundWork(ETTask<int> taskCompletionSource)
{
    await TimerComponent.Instance.WaitAsync(1500);
    // QUAN TRỌNG: Đảm bảo chỉ gọi SetResult/SetException một lần.
    // Nếu taskCompletionSource có thể null ở đây (do lỗi logic khác), cần kiểm tra null.
    taskCompletionSource?.SetResult(42);
}

// Gọi: ProcessWithPooledTask().Coroutine();

5. Phân tích Chuyên sâu (In-depth Analysis)

  • Object Pooling và Rủi ro:

    • Cơ chế: Khi ETTask.Create(true) hoặc ETTask<T>.Create(true) được gọi, nó cố gắng lấy một đối tượng ETTask đã được Recycle từ ConcurrentQueue. Nếu không có, nó tạo mới. Khi GetResult() được gọi trên một pooled task đã thành công (Succeeded), hoặc khi lỗi được xử lý trong GetResult (trạng thái Faulted), phương thức Recycle() được gọi để đặt lại trạng thái (Pending, callback = null, value = default) và đưa task trở lại queue (nếu queue chưa quá đầy).

    • Rủi ro lớn nhất: Nếu bạn giữ một tham chiếu đến một pooled ETTask (ví dụ: biến tcs trong Ví dụ 3) và await nó, sau khi await hoàn thành, đối tượng ETTask đó có thể đã được trả về pool và được tái sử dụng ở một nơi khác. Nếu bạn cố gắng tương tác tiếp với biến tcs cũ đó (ví dụ gọi lại SetResult, kiểm tra trạng thái), bạn có thể đang thao tác trên một task hoàn toàn khác, gây ra lỗi logic nghiêm trọng và khó dò tìm.

    • Quy tắc vàng: Sau khi await một pooled ETTask, không bao giờ sử dụng lại biến tham chiếu đến ETTask đó nữa. Việc lấy kết quả hoặc xử lý lỗi đã được thực hiện bên trong GetResult (được gọi bởi await).

    • Cảnh báo trong code: Mã nguồn gốc đã có những bình luận cảnh báo rõ ràng về việc này. Hãy luôn ghi nhớ chúng.

  • AsyncMethodBuilderStateMachineWrap:

    • Các Builder (ví dụ: ETAsyncTaskMethodBuilder) không chỉ tạo ETTask mà còn quản lý việc liên kết continuation (hàm MoveNext của state machine) với ETTask.

    • Khi AwaitOnCompleted hoặc AwaitUnsafeOnCompleted được gọi, builder sẽ đăng ký MoveNext của state machine làm callback cho ETTask đang được await.

    • StateMachineWrap<T> được sử dụng để bọc (wrap) state machine (thường là struct) vào một đối tượng class. Mục đích chính của việc này là để có thể tái sử dụng các đối tượng wrapper này thông qua pooling (StateMachineWrap<T>.FetchRecycle), giảm cấp phát bộ nhớ cho mỗi lần gọi hàm async. Delegate MoveNext được cache lại trong wrapper. Khi hàm async hoàn thành (SetResult/SetException trong builder), wrapper này cũng được Recycle.

  • ETVoid vs async void: async ETVoid tương tự như async void chuẩn, dùng cho các event handler hoặc các tác vụ "bắn và quên". Tuy nhiên, nó nguy hiểm vì các exception không được bắt bởi lời gọi await sẽ bị đưa thẳng đến ETTask.ExceptionHandler, có thể làm crash ứng dụng nếu không được xử lý đúng cách. Hạn chế tối đa việc sử dụng async ETVoid. Nếu cần chạy một tác vụ nền mà không cần chờ, hãy dùng MyAsyncETTask().Coroutine();.

  • ETTaskCompleted: Là một struct nhẹ, luôn ở trạng thái hoàn thành. Nó hữu ích khi cần trả về một ETTask đã biết là hoàn thành ngay lập tức mà không cần cấp phát đối tượng ETTask thực sự.

  • ETCancellationToken: Là một cơ chế hủy tùy chỉnh, không dựa trên System.Threading.CancellationTokenSource. Nó sử dụng một HashSet<Action> để lưu các callback hủy. Khi Cancel() được gọi, tất cả các action đã đăng ký sẽ được thực thi. Cần đảm bảo Remove được gọi khi tác vụ hoàn thành hoặc không cần hủy nữa để tránh memory leak.

6. Các Thành phần Chính (Key Components / API Reference)

  • ETTask:

    • Lớp đại diện cho một hoạt động bất đồng bộ không trả về giá trị.

    • static ETTask Create(bool fromPool = false): Tạo hoặc lấy từ pool một ETTask.

    • ETTask GetAwaiter(): Lấy awaiter (chính nó) để sử dụng với await.

    • bool IsCompleted: Kiểm tra xem task đã hoàn thành chưa.

    • void GetResult(): Lấy kết quả (hoặc ném exception nếu có lỗi). Được await gọi ngầm. Xử lý việc recycle nếu là pooled task.

    • void SetResult(): Đánh dấu task hoàn thành thành công và gọi callback.

    • void SetException(Exception e): Đánh dấu task hoàn thành với lỗi và gọi callback.

    • void Coroutine(): Bắt đầu thực thi task mà không cần await (fire-and-forget an toàn hơn ETVoid).

    • static Action<Exception> ExceptionHandler: Delegate toàn cục để xử lý các exception không bị bắt bởi ETVoid hoặc các lỗi khác trong hệ thống ETTask.

  • ETTask<T>:

    • Tương tự ETTask nhưng cho hoạt động trả về giá trị kiểu T.

    • static ETTask<T> Create(bool fromPool = false): Tạo hoặc lấy từ pool một ETTask<T>.

    • T GetResult(): Lấy giá trị kết quả kiểu T (hoặc ném exception). Được await gọi ngầm. Xử lý recycle.

    • void SetResult(T result): Đánh dấu task hoàn thành thành công với giá trị result.

  • ETVoid:

    • Struct dùng cho các phương thức async ETVoid.

    • void Coroutine(): Không làm gì cả (vì ETVoid không cần được quản lý sau khi gọi).

    • Lưu ý: Exception trong async ETVoid sẽ đi đến ETTask.ExceptionHandler.

  • ETTaskCompleted:

    • Struct đại diện cho một tác vụ đã hoàn thành ngay lập tức. Luôn IsCompleted == true.

  • ETCancellationToken:

    • Lớp quản lý việc hủy tác vụ.

    • void Add(Action callback): Thêm callback sẽ được gọi khi hủy.

    • void Remove(Action callback): Gỡ bỏ callback.

    • void Cancel(): Kích hoạt việc hủy, gọi tất cả các callback đã đăng ký.

    • bool IsDispose(): Kiểm tra xem token đã được hủy (và các callback đã được gọi) chưa.

  • ETTaskHelper:

    • Lớp chứa các phương thức tiện ích tĩnh.

    • static ETTask WaitAll(tasks): Chờ tất cả task hoàn thành.

    • static ETTask WaitAny(tasks): Chờ một task bất kỳ hoàn thành.

  • AsyncETTaskMethodBuilder, AsyncETTaskMethodBuilder<T>, AsyncETVoidMethodBuilder, AsyncETTaskCompletedMethodBuilder:

    • Các struct nội bộ được trình biên dịch sử dụng để xây dựng state machine cho các phương thức async tương ứng. Người dùng thông thường không trực tiếp tương tác với chúng.

  • StateMachineWrap<T>:

    • Lớp nội bộ để bọc và pooling các state machine wrapper.

7. Lưu ý Quan trọng (Important Notes & Caveats)

  • HẾT SỨC CẨN THẬN KHI DÙNG OBJECT POOLING (fromPool = true): Luôn nhớ rằng sau khi await một pooled task, tham chiếu gốc của bạn đến task đó không còn đáng tin cậy.

  • Tránh dùng async ETVoid: Ưu tiên async ETTask và gọi .Coroutine() nếu bạn muốn chạy nền mà không chờ kết quả.

  • Quản lý ETCancellationToken: Đảm bảo Remove callback khi không cần thiết nữa để tránh leak.

  • Thread Safety: ETTask được thiết kế chủ yếu cho môi trường đơn luồng hoặc mô hình Actor của ET. Việc sử dụng hoặc hoàn thành ETTask từ nhiều luồng khác nhau mà không có cơ chế đồng bộ hóa phù hợp có thể gây ra lỗi.

8. Tham khảo thêm (Further Reading)