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/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ể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
?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ớiSynchronizationContext
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/await
vàAsyncMethodBuilder
: Khi bạn viết một phương thứcasync ETTask
hoặ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ộtawait
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),
ETTask
vàETTask<T>
có thể được tái sử dụng. Khi mộtETTask
được tạo vớifromPool = true
và 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ủaETTask
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ơ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 ETTask
cho 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 ETVoid
cho các hàm "fire-and-forget" (không cầnawait
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ộtETTask
mớ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óaawait
như vớiTask
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ặcETTask<T>.Create(true)
được gọi, nó cố gắng lấy một đối tượngETTask
đã đượcRecycle
từ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ếntcs
trong Ví dụ 3) vàawait
nó, sau khiawait
hoà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ếntcs
cũ đó (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
await
mộ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.
AsyncMethodBuilder
vàStateMachineWrap
:Các
Builder
(ví dụ:ETAsyncTaskMethodBuilder
) không chỉ tạoETTask
mà còn quản lý việc liên kết continuation (hàmMoveNext
của state machine) vớiETTask
.Khi
AwaitOnCompleted
hoặcAwaitUnsafeOnCompleted
được gọi, builder sẽ đăng kýMoveNext
củ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>.Fetch
và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àmasync
hoàn thành (SetResult
/SetException
trong builder), wrapper này cũng đượcRecycle
.
ETVoid
vsasync 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ọiawait
sẽ 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ượngETTask
thự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). Đượcawait
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ầ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ởiETVoid
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ể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). Đượcawait
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 đế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
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 khiawait
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ênasync 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ảoRemove
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ànhETTask
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.