ETTask 核心机制与技术实现
ETTask 是 ET 框架自研的轻量级异步协程基元,旨在解决 C# 原生Task在单线程游戏逻辑中存在的同步上下文(SynchronizationContext)开销大、内存分配频繁及回调地狱问题。它基于状态机原理,配合对象池技术,实现了零 GC 压力的高并发异步调度 。
1. 核心设计原理
ETTask 摒弃了 .NET 原生的SynchronizationContext机制,采用单线程事件循环 + 续体直调的模式。
| 特性维度 | C# 原生 Task | ET 框架 ETTask | 优势解析 |
|---|---|---|---|
| 调度机制 | 依赖SynchronizationContext将续体(Continuation)封送回主线程,涉及队列操作和委托分配 。 | 无同步上下文。直接在当前单线程上下文中执行续体,无需线程切换或队列排队 。 | 消除线程上下文切换开销,提升高频调用下的吞吐量。 |
| 内存管理 | 每次异步操作通常产生新的Task对象和闭包,导致高频 GC 。 | 对象池化。ETAsyncTaskMethodBuilder和Awaiter均从对象池复用,大幅降低 GC 压力 。 | 适合游戏服务器长运行场景,避免内存抖动。 |
| 执行模型 | 多线程安全,但需锁保护状态 。 | 单线程专用。假设运行环境为单线程(如 Fiber),移除所有锁机制,极致简化逻辑 。 | 避免死锁风险,代码执行路径更短。 |
| 异常处理 | 抛出AggregateException或直接抛异常,需 try-catch 包裹 。 | 内部集成异常捕获,支持统一错误码返回,适配游戏业务逻辑 。 | 简化业务层的错误处理流程。 |
2. 技术实现细节
2.1 异步方法构建器 (ETAsyncTaskMethodBuilder)
ET 框架重写了 C# 的异步状态机入口。当编译器遇到async ETTask方法时,会使用ETAsyncTaskMethodBuilder而非原生的AsyncTaskMethodBuilder。
- 启动阶段:从对象池获取 Builder 实例,初始化状态机。
- 等待阶段:当遇到
await时,检查被等待对象(如 IO 操作)是否完成。若未完成,将当前状态机注册为回调,并立即返回控制权给主循环;若已完成,直接同步执行后续代码 。 - 恢复阶段:IO 完成后,通过
OneThreadSynchronizationContext或直接调用Run()方法,在无队列阻塞的情况下直接恢复状态机执行 。
2.2 续体直调与零上下文
原生await在恢复时会检查SynchronizationContext,若有则通过Post方法将续体放入消息队列。ETTask 移除了这一层:
- 直调机制:续体函数直接作为普通方法调用,避免了委托封装和队列查找的开销 。
- 单线程假设:由于 ET 服务器每个进程(Fiber)是单线程运行的,不需要考虑多线程竞争,因此不需要锁来保护状态机的状态流转 。
3. 代码实战与对比
以下示例展示了如何使用 ETTask 处理登录逻辑,并对比了其与原生 Task 的差异。
using ET; using System.Threading.Tasks; // 【正确写法】ET 框架标准异步处理 // 返回值必须是 ETTask 或 ETTask<T>,严禁使用 void async 或 Task public class LoginHandler : AMRpcHandler<C2R_Login, R2C_Login> { // 标记为 async,返回类型为 ETTask protected override async ETTask Run(Session session, C2R_Login request, Action<R2C_Login> response) { R2C_Login reply = new R2C_Login(); try { // 1. 模拟数据库异步查询 // await 挂起当前纤程,线程去处理其他请求,不阻塞主线程 var accountInfo = await AccountModel.GetAsync(request.account); if (accountInfo == null) { reply.ErrorCode = 101; response(reply); return; // 直接返回,结束当前纤程 } // 2. 模拟跨进程 Actor 调用 // 内部自动处理消息序列化、发送、等待响应、反序列化 var gateKey = await RealmActorComponent.Instance.GetGateKey(request.account); reply.GateKey = gateKey; reply.ErrorCode = 0; } catch (System.Exception e) { Log.Error(e); reply.ErrorCode = 500; } // 3. 发送响应 response(reply); // 4. 显式释放 Session (根据业务需求) session.Dispose(); } } // 【错误示范】原生 Task 在 ET 中的潜在问题 public class BadLoginHandler { // 错误点:返回 Task 会导致框架无法正确管理对象池和异常捕获 protected async Task RunBad(Session session) { // 错误点:原生 Task 可能触发 SynchronizationContext 上下文切换,增加不必要的开销 var data = await Task.Delay(100); } }4. 关键注意事项与避坑指南
在使用 ETTask 时,必须严格遵守以下规范以避免内存泄漏或逻辑错误:
严禁混用 Task 与 ETTask:
- 所有异步方法返回值必须为
ETTask或ETTask<T>。若在async ETTask方法中await一个原生的Task,虽然能编译通过,但会失去对象池优化且可能引发上下文调度问题 。 - 若必须调用返回
Task的第三方库,需使用task.ToETTask()扩展方法进行转换(框架通常提供此类适配)。
- 所有异步方法返回值必须为
避免 Async Void:
- 严禁使用
async void。这会导致异常无法被框架捕获,且无法追踪生命周期,极易导致服务器崩溃或逻辑中断 。
- 严禁使用
对象池回收意识:
- 虽然 ETTask 自动管理大部分资源,但在手动创建复杂异步状态或自定义 Awaitable 对象时,需确保实现
IDisposable或在finally块中归还对象池,防止内存泄漏 。
- 异常传播机制:
- ETTask 内部捕获的异常通常会封装到 Rpc 响应的
ErrorCode中,而不是直接抛出崩溃。开发者需在try-catch块中显式处理业务异常,确保response回调一定被执行,否则客户端将一直等待超时 。
5. 总结
ETTask 是 ET 框架高性能的基石之一。它通过去除同步上下文、引入对象池和单线程直调三大手段,将 C# 的异步编程改造为适合游戏服务器的高性能协程模型。开发者只需遵循async ETTask的编码规范,即可在享受async/await语法糖带来的线性代码可读性的同时,获得接近原生同步代码的执行效率 。
参考来源
- 【ET框架】基础及实践难题解答
- ET服务器框架学习笔记-杂记(ETTask,async,await)
- 什么是协程:从回调嵌套到 ET 框架的 ETTask 异步模型
- ET框架协程进阶:用 C async/await 与 TaskCompletionSource 告别回调地狱,走向零同步上下文的 ETTask
- ET框架避坑指南:Actor消息、ETTask异步与组件生命周期,新手最易踩的10个坑
- ET框架协程进阶:用 async/await 与 TaskCompletionSource 告别回调地狱,理解 ETTask 的单线程异步设计