尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
ET框架协程进阶:用 C async/await 与 TaskCompletionSource 告别回调地狱,走向零同步上下文的 ETTask
ET框架协程进阶用 C# async/await 与 TaskCompletionSource 告别回调地狱走向零同步上下文的 ETTask【免费下载链接】ETUnity3D Client And C# Server Framework项目地址: https://gitcode.com/GitHub_Trending/et/ET本篇指南以 ETUnity3D Client And C# Server Framework官方文档《2.2 更好的协程》为主线讲解如何借助 C# 的async/await语法与TaskCompletionSource把“一串回调”式的传统协程改写成直观的同步写法进一步说明 .NETSynchronizationContext在回主线程中的作用、SynchronizationContext.SetSynchronizationContext的用法与隐患并最终引出 ET 框架自研、完全绕开同步上下文的ETTask及其高效实现原理。读完你将掌握用 TCS 封装任意异步回调、利用同步上下文切回主线程、以及理解 ET 单线程模型下为什么ETTask更简洁高效。一、为什么需要“更好的协程”上文《2.1 CSharp 的协程》提到一串回调就是协程。但回调嵌套的写法在增加逻辑、插入逻辑时极易出错——每插入一步都要手工改写外层回调缩进越来越深流程被撕裂成碎片。我们需要一种语法把“异步回调的形式”改写成“同步的形式”。C# 早已为此设计好了async/await配合TaskCompletionSourceTCS即可把任意基于回调的异步操作包装成可await的Task。二、用 TaskCompletionSource 把回调改写成 await 形式文档example2_2给出了一个最小可运行的完整示例。其核心思路是用TaskCompletionSourcebool替代之前传入的Action回调参数WaitTimeAsync返回tcs.Task等待线程结束时调用tcs.SetResult(true)而不是action()协程方法前加await把一连串回调“拍平”成同步式顺序代码。// example2_2 class Program { private static int loopCount 0; static void Main(string[] args) { OneThreadSynchronizationContext _ OneThreadSynchronizationContext.Instance; Console.WriteLine($主线程: {Thread.CurrentThread.ManagedThreadId}); Crontine(); while (true) { OneThreadSynchronizationContext.Instance.Update(); Thread.Sleep(1); loopCount; if (loopCount % 10000 0) { Console.WriteLine($loop count: {loopCount}); } } } private static async void Crontine() { await WaitTimeAsync(5000); Console.WriteLine($当前线程: {Thread.CurrentThread.ManagedThreadId}, WaitTimeAsync finsih loopCount的值是: {loopCount}); await WaitTimeAsync(4000); Console.WriteLine($当前线程: {Thread.CurrentThread.ManagedThreadId}, WaitTimeAsync finsih loopCount的值是: {loopCount}); await WaitTimeAsync(3000); Console.WriteLine($当前线程: {Thread.CurrentThread.ManagedThreadId}, WaitTimeAsync finsih loopCount的值是: {loopCount}); } private static Task WaitTimeAsync(int waitTime) { TaskCompletionSourcebool tcs new TaskCompletionSourcebool(); Thread thread new Thread(()WaitTime(waitTime, tcs)); thread.Start(); return tcs.Task; } /// summary /// 在另外的线程等待 /// /summary private static void WaitTime(int waitTime, TaskCompletionSourcebool tcs) { Thread.Sleep(waitTime); // 将tcs扔回主线程执行 OneThreadSynchronizationContext.Instance.Post(otcs.SetResult(true), null); } }这段代码的改进点一目了然Crontine中三次await依次等待 5000/4000/3000 毫秒每次等待结束后打印当前线程与loopCount的值代码形态与同步顺序逻辑完全一致。await之后的语句本质上是编译器生成的“续体continuation”但开发者不再需要手工管理回调。需要说明的是这里WaitTime里的tcs.SetResult被通过OneThreadSynchronizationContext.Instance.Post主动扔回主线程执行这样await之后的代码就仍运行在主线程上Console.WriteLine打印出的线程号与主线程一致同时loopCount也在持续增长主循环while (true)每秒约执行 1000 次Thread.Sleep(1)空转。三、设置同步上下文让回调自动回到主线程示例中WaitTime需要手工把tcs.SetResult扔回主线程比较繁琐。微软提供了更简单的方案在主线程设置好同步上下文即可参考example2_2_2// example2_2_2 SynchronizationContext.SetSynchronizationContext(OneThreadSynchronizationContext.Instance);设置之后WaitTime中直接调用tcs.SetResult(true)即可回调会被 .NET 的 await 机制自动投递到同步上下文而同步上下文又是在主线程中被取出并执行回调的因此自动完成了“回到主线程”的操作private static void WaitTime(int waitTime, TaskCompletionSourcebool tcs) { Thread.Sleep(waitTime); tcs.SetResult(true); }背后的原理是await状态机在续体执行前会检查当前是否存在SynchronizationContext如果存在则通过context.Post(...)把续体投递进去而OneThreadSynchronizationContext.Update()在主循环中被反复调用负责把队列中的回调逐一取出执行。若不设置同步上下文await之后打印出的当前线程往往就不再是主线程——这其实是大多数第三方库与 .NET 内置库的默认行为不回调到主线程。因此使用这些库时需要自行设置同步上下文或在回调中手工切回主线程。3.1 从源码看 ET 的同步上下文实现ET 框架中的ThreadSynchronizationContextPackages/cn.etetet.core/Scripts/Core/Share/ThreadSynchronizationContext.cs正是文档中OneThreadSynchronizationContext思想的工程化实现public class ThreadSynchronizationContext : SynchronizationContext { // 线程同步队列,发送接收socket回调都放到该队列,由poll线程统一执行 private readonly ConcurrentQueueAction queue new(); public void Update() { while (true) { if (!this.queue.TryDequeue(out Action a)) { return; } try { a(); } catch (Exception e) { Log.Error(e); } } } public override void Post(SendOrPostCallback callback, object state) { this.Post(() callback(state)); } public void Post(Action action) { this.queue.Enqueue(action); } }从源码结构可以看清三点设计无锁队列使用ConcurrentQueueAction存放投递进来的回调生产者在任意线程Post消费者通常为主线程或 poll 线程在Update中批量取空队列统一执行点回调都汇聚到Update()里串行执行保证回调按投递顺序、在单一线程内被处理天然避免竞态异常兜底Update中 try/catch 捕获每个回调的异常并走Log.Error避免单个回调异常拖垮整个线程的消息循环。在 ET 的多线程 Fiber 架构中MainThreadSchedulerPackages/cn.etetet.core/Scripts/Core/Share/World/Fiber/MainThreadScheduler.cs在AddToScheduler时会把fiber.ThreadSynchronizationContext.ThreadId设为当前主线程 id由主线程的Update()驱动各 Fiber 的同步上下文从而让所有投递到该上下文中的回调统一在主线程执行。四、同步上下文方案的局限单线程游戏逻辑下的“多余”文档作者指出同步上下文这个设计“本人觉得没有必要交由库的开发者去实现更好”。理由非常务实尤其贴合游戏开发场景游戏逻辑全部是单线程的逻辑层不需要线程安全也不存在跨线程竞争线程调度由引擎/框架自身保证每次回调都走一遍同步上下文是多余的Post入队、主循环Update出队、再执行多了一次队列周转和一次间接调用纯属性能损耗调试与阅读成本上升回调经过上下文中转后调用栈被切断异常定位更困难。因此 ET 框架提供了不使用同步上下文的实现ETTask代码更简洁、更高效。五、ETTask单线程模型下更高效的协程基元ETTask是 ET 框架的核心异步基元完整实现位于 Packages/cn.etetet.core/Scripts/Core/Share/ETTask/ETTask.cs。它与Task的关键区别在于它不依赖SynchronizationContext也不做线程调度——续体在SetResult调用的线程上被直接执行不再经过任何队列中转。5.1 轻量的 awaiter 实现ETTask实现了ICriticalNotifyCompletion与自定义的IETTask接口并标注了[AsyncMethodBuilder(typeof(ETAsyncTaskMethodBuilder))]使编译器可以把它当作与Task同等的 awaitable 类型[AsyncMethodBuilder(typeof (ETAsyncTaskMethodBuilder))] public partial class ETTask: ICriticalNotifyCompletion, IETTask { private bool fromPool; private AwaiterStatus state; private object callback; // Action or ExceptionDispatchInfo public ETTask GetAwaiter() { return this; } public bool IsCompleted { get { return this.state ! AwaiterStatus.Pending; } } public void UnsafeOnCompleted(Action action) { if (this.state ! AwaiterStatus.Pending) { action?.Invoke(); return; } this.callback action; } ... }GetAwaiter()直接返回自身this省去了TaskAwaiter等包装对象的分配UnsafeOnCompleted只保存一个Action回调不经过 SynchronizationContext状态只有Pending / Succeeded / Faulted三种配合SetResult()/SetException()完成状态迁移ETTask.cspublic void SetResult() { if (this.state ! AwaiterStatus.Pending) { throw new InvalidOperationException(TaskT_TransitionToFinal_AlreadyCompleted); } this.state AwaiterStatus.Succeeded; Action c this.callback as Action; this.callback null; c?.Invoke(); // 直接同步执行续体不经过任何队列 }可以看到SetResult直接把保存的续体c?.Invoke()执行掉这就是“不使用同步上下文”的直接体现。5.2 编译器的异步方法构造器ETAsyncTaskMethodBuilder为了让async ETTask这样的写法能通过编译ET 在 Packages/cn.etetet.core/Scripts/Core/Share/ETTask/AsyncETTaskMethodBuilder.cs 中自定义了ETAsyncTaskMethodBuilder实现IAsyncStateMachine的全部 8 个约定方法。值得注意的细节Create()中通过ETTask.Create(true)从对象池获取 ETTask减少 GC 分配AwaitUnsafeOnCompleted中使用StateMachineWrapTStateMachine.Fetch复用状态机包装对象Task属性直接暴露内部tcs外部await的就是它。这套构造器把 .NET 默认的AsyncTaskMethodBuilder它会悄悄引入SynchronizationContext检查与Task分配替换成了 ET 自己的零上下文、对象池化实现是“更简洁更高效”的底层保障。5.3 何时使用 ETTask在 ET 中所有逻辑层协程都应使用ETTask/ETTaskT例如private async ETTask DownloadAndShow() { byte[] bytes await LoadFromNetworkAsync(); Show(bytes); } private async ETTaskbyte[] LoadFromNetworkAsync() { // 网络层完成时直接 SetResult续体在同一线程继续执行 return await SomeNetworkRequest(); }正因为 ET 的逻辑是单线程模型续体在哪个线程上被SetResult就在哪个线程上继续执行不需要也不能依赖同步上下文来回切换从而获得最直接的性能收益。六、小结从回调 → Task → ETTask 的演进脉络阶段形式优点缺点回调式协程回调套回调原生、直接增删逻辑易错、流程破碎Task TCS 同步上下文await tcs.Task同步写法、线程可控需设置上下文、回调经队列中转、有额外开销ETTaskawait自定义 awaitable单线程直通、对象池化、零上下文仅适用于 ET 单线程逻辑模型这条演进路径正是《2.2 更好的协程》一文的精髓先用TaskCompletionSource把回调封装成Task用await获得同步写法再通过SynchronizationContext.SetSynchronizationContext解决回主线程问题最终意识到在游戏单线程逻辑下同步上下文是多余的于是有了 ET 自研的ETTask。后续章节如《2.3 单线程异步》将在此基础上进一步展开单线程异步模型读者可继续深入 ET 的 Fiber、Entity 与异步框架。如果你希望在自己的项目里复刻这套方案建议直接从仓库的 ETTask.cs 与 AsyncETTaskMethodBuilder.cs 开始阅读它们是理解 ET 异步体系的最佳入口。【免费下载链接】ETUnity3D Client And C# Server Framework项目地址: https://gitcode.com/GitHub_Trending/et/ET创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED

相关推荐

Nixpkgs buildEnv 详解:用符号链接构造可复用的用户环境与软件包扩展

Nixpkgs buildEnv 详解:用符号链接构造可复用的用户环境与软件包扩展

Nixpkgs buildEnv 详解:用符号链接构造可复用的用户环境与软件包扩展 【免费下载链接】nixpkgs Nix Packages collection & NixOS 项目地址: https://gitcode.com/GitHub_Trending/ni/nixpkgs buildEnv 是 Nixpkgs 中用于构造"目录 符号链接"…

📅 2026/9/15 21:05:49
FGUI字体描边实战:原理、方案与性能优化

FGUI字体描边实战:原理、方案与性能优化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/9/15 21:05:49
Wasp 生产环境数据库部署指南:PostgreSQL 连接、Prisma 迁移与故障排查

Wasp 生产环境数据库部署指南:PostgreSQL 连接、Prisma 迁移与故障排查

Wasp 生产环境数据库部署指南:PostgreSQL 连接、Prisma 迁移与故障排查 【免费下载链接】wasp The batteries-included full-stack framework for the AI era. Develop JS/TS web apps (React, Node.js, and Prisma) using declarative code that abstracts away co…

📅 2026/9/15 21:00:48
MORE NEWS

更多资讯

📰

Astryx 应用工作区解析:/apps 下文档站、示例工程、Sandbox 与 Storybook 的定位与协作

Astryx 应用工作区解析:/apps 下文档站、示例工程、Sandbox 与 Storybook 的定位与协作 【免费下载链接】astryx An open source design system thats fully customizable and agent ready 项目地址: https://gitcode.com/GitHub_Trending/as/astryx 导读 A…

📰

SpringBoot+Vue图书商城系统源码解析:从架构到部署

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

百度旋转图片验证码识别与自动化适配实战

百度旋转图片验证码,应该有不少朋友在登录百度账号、转存网盘资源或者发帖时候遇到过。它跟常见的滑块验证不太一样,不是把碎片推到对应凹槽里,而是给你一张倾斜的图片,让你用鼠标或者手指把它转到水平位置。这个交互看起来很简单…

📰

Hypermesh与Ls-Dyna联合碰撞仿真:从网格划分到结果分析全流程实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

计算机视觉与光谱分析在食品包装重金属检测中的应用

1. 项目背景与行业痛点食品接触材料的安全性问题近年来受到广泛关注,特别是包装袋中的重金属迁移风险。传统检测方式存在几个明显短板:人工检测效率低下:一个完整样品从采样到出具报告通常需要3-5个工作日主观性强:不同检测员对色…

📰

腾讯AI办公工作台实操指南:提示词结构化与办公闭环校验

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬