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
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/awaitquen 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ểuT),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?Taskcủa .NET được thiết kế cho nhiều kịch bản đa luồng phức tạp, đi kèm vớiSynchronizationContextvà 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/awaitvàAsyncMethodBuilder: Khi bạn viết một phương thứcasync ETTaskhoặcasync 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ộtawaithoà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),
ETTaskvàETTask<T>có thể được tái sử dụng. Khi mộtETTaskđược tạo vớifromPool = truevà hoàn thành (thông quaGetResult), 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ủaETTaskthườ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ơiawaitđượ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 ETTaskcho các hàm không trả về giá trị nhưng cần đượcawait.Sử dụng
async ETTask<T>cho các hàm trả về giá trị kiểuT.Sử dụng
async ETVoidcho các hàm "fire-and-forget" (không cầnawaitvà không quan tâm kết quả/lỗi - thận trọng khi sử dụng).
Tạo
ETTaskthủ công (ít dùng hơn):ETTask.Create(bool fromPool = false): Tạo mộtETTaskmới.ETTask<T>.Create(bool fromPool = false): Tạo mộtETTask<T>mới.Sử dụng
SetResult()hoặcSetResult(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óaawaitnhư vớiTaskthô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ứcVí 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ặcETTask<T>.Create(true)được gọi, nó cố gắng lấy một đối tượngETTaskđã đượcRecycletừConcurrentQueue. Nếu không có, nó tạo mới. KhiGetResult()được gọi trên một pooled task đã thành công (Succeeded), hoặc khi lỗi được xử lý trongGetResult(trạng tháiFaulted), phương thứcRecycle()đượ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ếntcstrong Ví dụ 3) vàawaitnó, sau khiawaithoàn thành, đối tượngETTaskđó 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ếntcscũ đó (ví dụ gọi lạiSetResult, 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
awaitmột pooledETTask, không bao giờ sử dụng lại biến tham chiếu đếnETTaskđó nữa. Việc lấy kết quả hoặc xử lý lỗi đã được thực hiện bên trongGetResult(được gọi bởiawait).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.
AsyncMethodBuildervàStateMachineWrap:Các
Builder(ví dụ:ETAsyncTaskMethodBuilder) không chỉ tạoETTaskmà còn quản lý việc liên kết continuation (hàmMoveNextcủa state machine) vớiETTask.Khi
AwaitOnCompletedhoặcAwaitUnsafeOnCompletedđược gọi, builder sẽ đăng kýMoveNextcủa state machine làm callback choETTaskđ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>.FetchvàRecycle), giảm cấp phát bộ nhớ cho mỗi lần gọi hàmasync. DelegateMoveNextđược cache lại trong wrapper. Khi hàmasynchoàn thành (SetResult/SetExceptiontrong builder), wrapper này cũng đượcRecycle.
ETVoidvsasync void:async ETVoidtương tự nhưasync voidchuẩ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ọiawaitsẽ bị đưa thẳng đếnETTask.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ụngasync ETVoid. Nếu cần chạy một tác vụ nền mà không cần chờ, hãy dùngMyAsyncETTask().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ộtETTaskđã biết là hoàn thành ngay lập tức mà không cần cấp phát đối tượngETTaskthực sự.ETCancellationToken: Là một cơ chế hủy tùy chỉnh, không dựa trênSystem.Threading.CancellationTokenSource. Nó sử dụng mộtHashSet<Action>để lưu các callback hủy. KhiCancel()được gọi, tất cả các action đã đăng ký sẽ được thực thi. Cần đảm bảoRemoveđượ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ớiawait.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). Đượcawaitgọ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ầnawait(fire-and-forget an toàn hơnETVoid).static Action<Exception> ExceptionHandler: Delegate toàn cục để xử lý các exception không bị bắt bởiETVoidhoặc các lỗi khác trong hệ thống ETTask.
ETTask<T>:Tương tự
ETTasknhưng cho hoạt động trả về giá trị kiểuT.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ểuT(hoặc ném exception). Đượcawaitgọ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ìETVoidkhông cần được quản lý sau khi gọi).Lưu ý: Exception trong
async ETVoidsẽ đi đếnETTask.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
asynctươ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 khiawaitmộ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ênasync ETTaskvà 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ảoRemovecallback 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ànhETTasktừ 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.