[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"consumer-news-detail-832":3,"consumer-news-interaction-832":40,"consumer-news-related-832":43},{"detail":4,"item":35},{"card":5,"schemaVersion":22,"fields":23,"content":29},{"id":6,"kind":7,"targetType":8,"targetId":9,"subtype":7,"typeLabel":10,"title":11,"subtitle":12,"summary":13,"coverUrl":14,"badgeText":14,"href":15,"sourceName":12,"meta":16,"metrics":19,"tags":20,"resolved":21},"NEWS_ARTICLE:832","news","NEWS_ARTICLE",832,"资讯","用 runtime-async 写一个支持 async\u002Fawait 的轻量脚本引擎","博客园","起因：一个好奇 事情的开头很简单。 给 .NET 做过动态脚本的人大概都碰到过同一堵墙：表达式树也好、Reflection.Emit 也好，都很难支持 async\u002Fawait。原因不在语法，而在 await 的实现方式——C# 编译器要为每个 async 方法生成一个状态机结构体，把方法体切成若干片","","\u002Fnews\u002F832",[17,18],"2026","软件开发",{},[18],true,"consumer-content-detail-v1",{"sourceName":12,"authorName":24,"categoryName":18,"summary":13,"description":13,"publishTime":25,"updateTime":26,"sourceUrl":27,"language":28},"victor.x.qu","2026-09-02T17:20","2026-09-02T20:37:48","https:\u002F\u002Fwww.cnblogs.com\u002Ffs7744\u002Fp\u002F22811546","中文",{"format":30,"policy":31,"normalized":21,"html":32,"text":33,"wordCount":34,"hasBody":21},"HTML","NEWS_CONTENT_V1","\u003Ch2>起因：一个好奇\u003C\u002Fh2>\n\u003Cp>事情的开头很简单。\u003C\u002Fp>\n\u003Cp>给 .NET 做过动态脚本的人大概都碰到过同一堵墙：\u003Cstrong>表达式树也好、\u003Ccode>Reflection.Emit\u003C\u002Fcode> 也好，都很难支持 \u003Ccode>async\u003C\u002Fcode>\u002F\u003Ccode>await\u003C\u002Fcode>\u003C\u002Fstrong>。原因不在语法，而在 \u003Ccode>await\u003C\u002Fcode> 的实现方式——C# 编译器要为每个 \u003Ccode>async\u003C\u002Fcode> 方法生成一个状态机结构体，把方法体切成若干片，配上 \u003Ccode>AsyncTaskMethodBuilder\u003C\u002Fcode>、awaiter 字段、\u003Ccode>MoveNext\u003C\u002Fcode> 的巨型 switch。这套东西你在编译期用 Roslyn 生成很自然，但要在运行时用 IL 一条条 emit 出来，工作量和出错面都大到不合理。\u003C\u002Fp>\n\u003Cp>所以脚本引擎通常二选一：要么放弃 \u003Ccode>await\u003C\u002Fcode>，要么退回到解释执行（然后性能就没得谈了）。\u003C\u002Fp>\n\u003Cp>后来看到 runtime-async 这个提案，我的第一反应是——\u003Cstrong>如果状态机改由 JIT 来生成，那\"动态生成的代码\"和\"编译期生成的代码\"不就站在同一起跑线上了吗？\u003C\u002Fstrong> 换句话说：一个运行时 emit 出来的方法，能不能既真正支持 \u003Ccode>await\u003C\u002Fcode>，又保持接近手写 C# 的性能？\u003C\u002Fp>\n\u003Cp>这个好奇没法靠读文档回答，只能写代码试。于是有了 \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Ffs7744\u002FV.Script\" target=\"_blank\" rel=\"noopener noreferrer nofollow\">\u003Cstrong>V.Script\u003C\u002Fstrong>\u003C\u002Fa>：一个编译成委托、兼容 C# 类型系统的轻量脚本引擎。\u003C\u002Fp>\n\u003Cp>结论先放这里：\u003Cstrong>能，但代价在你可能没预料到的地方\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Cblockquote>\n \u003Ch3>关于版本，先说清楚\u003C\u002Fh3>\n \u003Cp>这个想法最早是冲着 .NET 10 去的——runtime-async 是在 .NET 10 周期里作为实验特性登场的。\u003C\u002Fp>\n \u003Cp>但\u003Cstrong>本文所有代码和所有数字，都是在 .NET 11.0.100-preview.7 上跑出来的\u003C\u002Fstrong>，仓库的 \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Ffs7744\u002FV.Script\u002Fblob\u002Fmain\u002Fglobal.json\" target=\"_blank\" rel=\"noopener noreferrer nofollow\">\u003Ccode>global.json\u003C\u002Fcode>\u003C\u002Fa> 也精确 pin 了这个预览版号。我没有在 .NET 10 上完整跑过下面这套验证，所以不替它背书；如果你在 .NET 10 上试，请自己先跑一遍仓库里的 \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Ffs7744\u002FV.Script\u002Ftree\u002Fmain\u002Ftools\u002FV.Script.RuntimeAsyncCheck\" target=\"_blank\" rel=\"noopener noreferrer nofollow\">\u003Ccode>tools\u002FV.Script.RuntimeAsyncCheck\u003C\u002Fcode>\u003C\u002Fa>。\u003C\u002Fp>\n \u003Cp>另外 \u003Ccode>AsyncHelpers\u003C\u002Fcode> 目前带着 \u003Ccode>SYSLIB5007\u003C\u002Fcode> 实验性标记，签名随时可能变。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Chr>\n\u003Ch2>runtime-async 是什么：一处 IL 层面的差异\u003C\u002Fh2>\n\u003Cp>先看传统的 \u003Ccode>async\u003C\u002Fcode> 方法在 IL 里长什么样。这是 \u003Ccode>async Task&lt;int&gt; F() =&gt; await GetAsync();\u003C\u002Fcode> 编译出来的东西的骨架：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>\u002F\u002F 编译器额外生成的类型\n.class nested private sealed beforefieldinit '&lt;F&gt;d__0'\n       extends [System.Runtime]System.ValueType\n       implements [System.Runtime]IAsyncStateMachine\n{\n    .field public int32 '&lt;&gt;1__state'\n    .field public valuetype AsyncTaskMethodBuilder`1&lt;int32&gt; '&lt;&gt;t__builder'\n    .field private valuetype TaskAwaiter`1&lt;int32&gt; '&lt;&gt;u__1'\n\n    .method private hidebysig newslot virtual final\n            instance void MoveNext() cil managed\n    {\n        \u002F\u002F 一个按 &lt;&gt;1__state 分派的大 switch，\n        \u002F\u002F 把原方法体切成若干段，每段之间保存\u002F恢复局部变量\n    }\n}\n\n\u002F\u002F 原方法只剩下引导代码\n.method public hidebysig static\n        class Task`1&lt;int32&gt; F() cil managed\n{\n    .custom instance void AsyncStateMachineAttribute::.ctor(class Type)\n    \u002F\u002F 构造状态机、Start、返回 builder.Task\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>要用 \u003Ccode>Reflection.Emit\u003C\u002Fcode> 复刻这一坨：你得定义一个额外的类型、实现 \u003Ccode>IAsyncStateMachine\u003C\u002Fcode>、自己做局部变量提升、自己切分基本块、自己写状态分派。这不是\"麻烦\"，这是重新实现一遍编译器里最难的一个 lowering。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>runtime-async 把这件事整个搬走了。\u003C\u002Fstrong> 同样的语义，IL 变成这样：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>.method public hidebysig static\n        class Task`1&lt;int32&gt; Run(class ScriptHost, !!TGlobals) cil managed async\n\u002F\u002F                                                                      ^^^^^\n\u002F\u002F                                    MethodImplAttributes.Async == 0x2000\n{\n    \u002F\u002F 直线 IL，没有状态机，没有额外类型\n    call     class Task`1&lt;int32&gt; Service::GetAsync()\n    call     !!0 AsyncHelpers::Await&lt;int32&gt;(class Task`1&lt;!!0&gt;)   \u002F\u002F ← 挂起点\n    ret                                                          \u002F\u002F ← 注意这里\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>三处关键差异：\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>方法上多了一个 \u003Ccode>async\u003C\u002Fcode> 实现标志\u003C\u002Fstrong>（\u003Ccode>MethodImplAttributes.Async\u003C\u002Fcode>，值 \u003Ccode>0x2000\u003C\u002Fcode>）。它告诉 JIT：这个方法体需要被改写成状态机。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>\u003Ccode>await\u003C\u002Fcode> 变成一次普通的 \u003Ccode>call\u003C\u002Fcode>\u003C\u002Fstrong>，调用 \u003Ccode>System.Runtime.CompilerServices.AsyncHelpers.Await&lt;T&gt;\u003C\u002Fcode>。JIT 认得这个调用，把它变成真正的挂起点。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>签名声明返回 \u003Ccode>Task&lt;int&gt;\u003C\u002Fcode>，但 IL 里 \u003Ccode>ret\u003C\u002Fcode> 弹出的是 \u003Ccode>int\u003C\u002Fcode>\u003C\u002Fstrong>。包装成 \u003Ccode>Task\u003C\u002Fcode> 是运行时的事，emit 方不用管。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>对一个代码生成器来说，这个差异是决定性的。在 V.Script 里，整个 \u003Ccode>await\u003C\u002Fcode> 的发射逻辑就是这么长：\u003C\u002Fp>\n\u003Cp>\u003Ca href=\"https:\u002F\u002Fgithub.com\u002Ffs7744\u002FV.Script\u002Fblob\u002Fmain\u002Fsrc\u002FV.Script\u002FEmit\u002FIlEmitter.Expressions.cs\" target=\"_blank\" rel=\"noopener noreferrer nofollow\">\u003Ccode>src\u002FV.Script\u002FEmit\u002FIlEmitter.Expressions.cs\u003C\u002Fcode>\u003C\u002Fa>：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>\u002F\u002F\u002F &lt;summary&gt;\n\u002F\u002F\u002F A suspension point. The JIT turns this call into the state machine, so the whole of\n\u002F\u002F\u002F &lt;c&gt;await&lt;\u002Fc&gt; support on the emit side is one operand plus one call.\n\u002F\u002F\u002F &lt;\u002Fsummary&gt;\nprivate void EmitAwait(BoundAwait await)\n{\n    EmitExpression(await.Operand);\n    _il.Emit(OpCodes.Call, await.AwaitHelper);\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>两行。\u003C\u002Fstrong> 绑定器那边也只是根据操作数类型在 \u003Ccode>Task\u003C\u002Fcode> \u002F \u003Ccode>Task&lt;T&gt;\u003C\u002Fcode> \u002F \u003Ccode>ValueTask\u003C\u002Fcode> \u002F \u003Ccode>ValueTask&lt;T&gt;\u003C\u002Fcode> 四个重载里挑一个。原本\"重新实现一遍 lowering\"的工作量，压缩成了\"选个重载再 emit 一条 call\"。\u003C\u002Fp>\n\u003Chr>\n\u003Ch2>但这里有一个坑：\u003Ccode>DynamicMethod\u003C\u002Fcode> 用不了\u003C\u002Fh2>\n\u003Cp>好消息说完了，说坏消息。\u003C\u002Fp>\n\u003Cp>动态生成方法最轻的载体是 \u003Ccode>DynamicMethod\u003C\u002Fcode>：不需要程序集、不需要类型，委托没人引用时自动回收。V.Script 的同步脚本就用它——一个脚本大约 1.3 KB、几微秒。\u003C\u002Fp>\n\u003Cp>问题是：\u003Cstrong>\u003Ccode>DynamicMethod\u003C\u002Fcode> 没有 \u003Ccode>SetImplementationFlags\u003C\u002Fcode>\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Cp>\u003Ca href=\"https:\u002F\u002Fgithub.com\u002Ffs7744\u002FV.Script\u002Fblob\u002Fmain\u002Ftools\u002FV.Script.RuntimeAsyncCheck\u002FProgram.cs\" target=\"_blank\" rel=\"noopener noreferrer nofollow\">\u003Ccode>tools\u002FV.Script.RuntimeAsyncCheck\u002FProgram.cs\u003C\u002Fcode>\u003C\u002Fa>：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>Check(\"DynamicMethod 无法标记 Async\",\n    typeof(DynamicMethod).GetMethod(\"SetImplementationFlags\") is null,\n    \"这一个 API 缺口决定了异步脚本必须用独占程序集\");\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>上面这行来自仓库里的 \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Ffs7744\u002FV.Script\u002Ftree\u002Fmain\u002Ftools\u002FV.Script.RuntimeAsyncCheck\" target=\"_blank\" rel=\"noopener noreferrer nofollow\">\u003Ccode>tools\u002FV.Script.RuntimeAsyncCheck\u003C\u002Fcode>\u003C\u002Fa>——一个专门用来验证 runtime-async 前提条件的小程序。这一个 API 缺口，直接决定了整个异步方案的形状：\u003Cstrong>异步脚本没法用 \u003Ccode>DynamicMethod\u003C\u002Fcode>，只能退到 \u003Ccode>AssemblyBuilder\u003C\u002Fcode> + \u003Ccode>MethodBuilder\u003C\u002Fcode>\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Cp>于是 V.Script 有了两个载体：\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>\u003C\u002Fth>\n   \u003Cth>同步脚本\u003C\u002Fth>\n   \u003Cth>异步脚本\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>载体\u003C\u002Ftd>\n   \u003Ctd>\u003Ccode>DynamicMethod\u003C\u002Fcode>\u003C\u002Ftd>\n   \u003Ctd>独占的 collectible 程序集\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>内存\u003C\u002Ftd>\n   \u003Ctd>~1.3 KB\u003C\u002Ftd>\n   \u003Ctd>~31 KB\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>回收\u003C\u002Ftd>\n   \u003Ctd>委托没人引用即回收\u003C\u002Ftd>\n   \u003Ctd>需要显式 \u003Ccode>Dispose\u003C\u002Fcode>，卸载整个程序集\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>能否标记 \u003Ccode>Async\u003C\u002Fcode>\u003C\u002Ftd>\n   \u003Ctd>❌\u003C\u002Ftd>\n   \u003Ctd>✅\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>这个\"一个 API 缺口 → 载体必须换 → 成本涨 24 倍\"的因果链，是这个项目里最贵的一课，后面性能一节还会再见到它。\u003C\u002Fp>\n\u003Cp>顺带一提，\u003Ccode>async\u003C\u002Fcode> lambda 也踩同一个坑：lambda 平时编译成 \u003Ccode>DynamicMethod\u003C\u002Fcode>，一旦写了 \u003Ccode>async\u003C\u002Fcode>，它就必须搬进程序集。所以\u003Cstrong>同步脚本里出现 \u003Ccode>async\u003C\u002Fcode> lambda，也会让这个脚本多出一个程序集\u003C\u002Fstrong>（脚本体本身仍是 \u003Ccode>DynamicMethod\u003C\u002Fcode>，只有这些 lambda 搬家）。\u003C\u002Fp>\n\u003Chr>\n\u003Ch2>长什么样\u003C\u002Fh2>\n\u003Cpre>\u003Ccode>using var engine = new ScriptEngine(ScriptOptions.Default);\n\n\u002F\u002F 同步：编译成委托，DynamicMethod 载体\nusing var rule = engine.Compile&lt;Order, bool&gt;(\n    \"Total &gt; 1000 &amp;&amp; Customer.IsVip\");\n\nbool ok = rule.Run(order);\n\n\u002F\u002F 异步：真正的 await，独占程序集载体\nusing var flow = engine.CompileAsync&lt;Context, int&gt;(\"\"\"\n    var total = 0;\n    for (var i = 0; i &lt; Ids.Length; i++)\n        total += await Service.GetAsync(Ids[i]);\n    return total;\n    \"\"\");\n\nint result = await flow.RunAsync(context);\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>Compile\u003C\u002Fcode> 和 \u003Ccode>CompileAsync\u003C\u002Fcode> 是\u003Cstrong>两个不同的入口\u003C\u002Fstrong>，不是一个方法加个开关——因为它们背后是两个载体、两套成本模型，让调用方知道自己在付什么代价比\"透明\"更重要。\u003C\u002Fp>\n\u003Chr>\n\u003Ch2>性能实测\u003C\u002Fh2>\n\u003Cp>环境：Windows 11 x64，.NET 11.0.100-preview.7，BenchmarkDotNet 短任务、进程内 toolchain。\u003C\u002Fp>\n\u003Ch3>同步执行：与手写 C# 持平\u003C\u002Fh3>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>场景\u003C\u002Fth>\n   \u003Cth>手写 C#\u003C\u002Fth>\n   \u003Cth>脚本\u003C\u002Fth>\n   \u003Cth>分配\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>decimal 公式\u003C\u002Ftd>\n   \u003Ctd>20.0 ns\u003C\u002Ftd>\n   \u003Ctd>21.0 ns\u003C\u002Ftd>\n   \u003Ctd>0 B\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>bool 规则\u003C\u002Ftd>\n   \u003Ctd>5.2 ns\u003C\u002Ftd>\n   \u003Ctd>8.7 ns\u003C\u002Ftd>\n   \u003Ctd>0 B\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>1000 次循环\u003C\u002Ftd>\n   \u003Ctd>3100 ns\u003C\u002Ftd>\n   \u003Ctd>3077 ns\u003C\u002Ftd>\n   \u003Ctd>0 B\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>这个结果没什么好惊讶的：两侧都是 JIT 编译的 IL，持平才是预期。1000 次循环那行脚本略快是噪声，不必当真。\u003C\u002Fp>\n\u003Ch3>异步执行：多一次委托调用的量级\u003C\u002Fh3>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>场景\u003C\u002Fth>\n   \u003Cth>手写 C#\u003C\u002Fth>\n   \u003Cth>脚本\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>单次 await\u003C\u002Ftd>\n   \u003Ctd>12.4 ns\u003C\u002Ftd>\n   \u003Ctd>25.9 ns\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>循环内 10 次 await\u003C\u002Ftd>\n   \u003Ctd>12.0 ns\u003C\u002Ftd>\n   \u003Ctd>24.6 ns\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>\u003Cstrong>这是本文最想给出的那个数字。\u003C\u002Fstrong> 一个运行时 emit 出来的异步方法，单次 \u003Ccode>await\u003C\u002Fcode> 是 25.9 ns——同一量级，不是\"慢一个数量级\"。\u003C\u002Fp>\n\u003Cp>两个必须说明的点，否则这张表会骗人：\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Cstrong>基线也开了 \u003Ccode>runtime-async=on\u003C\u002Fcode> 编译。\u003C\u002Fstrong> 如果拿脚本的 runtime-async 去比手写 C# 的经典状态机，比的是两种 lowering，不是这个引擎做得好不好。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>被 await 的 Task 都是已完成的\u003C\u002Fstrong>，量的是 runtime-async 的快路径，不含调度开销。真挂起的场景由调度器主导，两边差异会被淹没。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>还有一个数字值得单独讲：\u003Cstrong>在移除超时机制之前，同一场景要 241 ns。\u003C\u002Fstrong> 早期版本给每次异步调用都挂了 linked \u003Ccode>CancellationTokenSource\u003C\u002Fcode> + 定时器，光这套\"安全设施\"就吃掉了 90% 的时间。把它整个删掉、把取消交还给宿主之后才有上面的 25.9 ns。\u003Cstrong>异步的开销往往不在 \u003Ccode>await\u003C\u002Fcode> 本身，而在你顺手加在它周围的东西。\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Ch3>编译：代价全在这里\u003C\u002Fh3>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>场景\u003C\u002Fth>\n   \u003Cth>耗时\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>同步，小脚本\u003C\u002Ftd>\n   \u003Ctd>8.6 µs\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>同步，中等（5 条语句）\u003C\u002Ftd>\n   \u003Ctd>31.0 µs\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>\u003Cstrong>异步，小脚本\u003C\u002Fstrong>\u003C\u002Ftd>\n   \u003Ctd>\u003Cstrong>346 µs\u003C\u002Fstrong>\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>\u003Cstrong>异步，含 await 的循环\u003C\u002Fstrong>\u003C\u002Ftd>\n   \u003Ctd>\u003Cstrong>629 µs\u003C\u002Fstrong>\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>缓存命中\u003C\u002Ftd>\n   \u003Ctd>54.5 ns\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>\u003Cstrong>异步比同步贵约 40 倍\u003C\u002Fstrong>，而且这 40 倍跟 runtime-async 一点关系都没有——它全部来自 collectible 程序集的创建与卸载。也就是说：\u003C\u002Fp>\n\u003Cblockquote>\n \u003Cp>\u003Ccode>DynamicMethod\u003C\u002Fcode> 不能标记 \u003Ccode>Async\u003C\u002Fcode> 这一个 API 缺口，全部代价就体现在这张表上。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>如果哪天 \u003Ccode>DynamicMethod\u003C\u002Fcode> 补上 \u003Ccode>SetImplementationFlags\u003C\u002Fcode>，异步脚本的编译开销会直接掉回同步那一档，31 KB 的常驻内存也一起消失。\u003C\u002Fp>\n\u003Chr>\n\u003Ch2>局限：请务必读完这一节\u003C\u002Fh2>\n\u003Cp>前面都是好消息，这一节是买单的地方。\u003C\u002Fp>\n\u003Ch3>1. \u003Ccode>await\u003C\u002Fcode> 不能出现在 \u003Ccode>catch\u003C\u002Fcode> \u002F \u003Ccode>finally\u003C\u002Fcode> 里——\u003Cstrong>这会让进程直接崩溃\u003C\u002Fstrong>\u003C\u002Fh3>\n\u003Cp>这是最严重的一条。当前运行时对\u003Cstrong>处理器块内的挂起点\u003C\u002Fstrong>不提供保护：不是抛异常，不是返回错误，是\u003Cstrong>整个进程带着 \u003Ccode>0xC0000005\u003C\u002Fcode> 退出\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Cp>验证工具里这条 case 必须开子进程来跑，因为它没法在本进程里安全地测：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>OK  catch 内真正挂起的 await 会终止进程（预期如此）\n    子进程退出码 -1073741819；为 0 说明运行时行为已改变，需重新评估 VS3004\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>所以 V.Script 的绑定器\u003Cstrong>在编译期无条件拒绝\u003C\u002Fstrong>这种写法（诊断码 \u003Ccode>VS3004\u003C\u002Fcode>），不提供任何开关。\u003C\u002Fp>\n\u003Cp>这里有个反直觉的细节值得单独说：\u003Ccode>AsyncHelpers.Await\u003C\u002Fcode> 对\u003Cstrong>已完成的 Task 走快路径，根本不会到达挂起点\u003C\u002Fstrong>。所以你在 \u003Ccode>catch\u003C\u002Fcode> 里写 \u003Ccode>await SomethingAlreadyCompleted()\u003C\u002Fcode> 很可能跑得好好的——\u003Cstrong>直到某天那个 Task 真的挂起，然后线上进程没了\u003C\u002Fstrong>。正因为它\"偶尔能跑通\"，编译期拒绝才更有必要，而不是更没必要。\u003C\u002Fp>\n\u003Ch3>2. 异步脚本 31 KB 常驻，且必须显式释放\u003C\u002Fh3>\n\u003Cp>每个异步脚本一个 collectible 程序集。一万个异步脚本 ≈ 310 MB，而且只有在你 \u003Ccode>Dispose\u003C\u002Fcode> 掉脚本、且没有任何委托还引用着它时才会卸载。\u003C\u002Fp>\n\u003Cp>热更新场景要按代次换新，不能无限编译新版本。同步脚本没有这个问题。\u003C\u002Fp>\n\u003Ch3>3. 生成的程序集失去 \u003Ccode>skipVisibility\u003C\u002Fcode>\u003C\u002Fh3>\n\u003Cp>\u003Ccode>DynamicMethod\u003C\u002Fcode> 可以用 \u003Ccode>skipVisibility: true\u003C\u002Fcode> 访问 globals 类型的非公开成员;独立程序集不行。\u003C\u002Fp>\n\u003Cp>所以\u003Cstrong>异步脚本（以及带 \u003Ccode>async\u003C\u002Fcode> lambda 的同步脚本）只能访问公开成员\u003C\u002Fstrong>。给 lambda 加个 \u003Ccode>async\u003C\u002Fcode> 会改变它的可见性权限——这个副作用不明显，但确实存在。\u003C\u002Fp>\n\u003Ch3>4. 预览版依赖\u003C\u002Fh3>\n\u003Cul>\n \u003Cli>\u003Ccode>AsyncHelpers\u003C\u002Fcode> 带 \u003Ccode>SYSLIB5007\u003C\u002Fcode> 实验性标记，签名可能变。V.Script 把使用点全部收拢在 \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Ffs7744\u002FV.Script\u002Fblob\u002Fmain\u002Fsrc\u002FV.Script\u002FBinding\u002FAwaitHelpers.cs\" target=\"_blank\" rel=\"noopener noreferrer nofollow\">\u003Ccode>Binding\u002FAwaitHelpers.cs\u003C\u002Fcode>\u003C\u002Fa> 一个文件里，就是为了将来好改。\u003C\u002Fli>\n \u003Cli>\u003Ca href=\"https:\u002F\u002Fgithub.com\u002Ffs7744\u002FV.Script\u002Fblob\u002Fmain\u002Fglobal.json\" target=\"_blank\" rel=\"noopener noreferrer nofollow\">\u003Ccode>global.json\u003C\u002Fcode>\u003C\u002Fa> 精确 pin 了预览版号（\u003Ccode>rollForward\u003C\u002Fcode> 没法从正式版号回退到预览版），GA 后需要改。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>GA 后请重跑 \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Ffs7744\u002FV.Script\u002Ftree\u002Fmain\u002Ftools\u002FV.Script.RuntimeAsyncCheck\" target=\"_blank\" rel=\"noopener noreferrer nofollow\">\u003Ccode>tools\u002FV.Script.RuntimeAsyncCheck\u003C\u002Fcode>\u003C\u002Fa>\u003C\u002Fstrong>，整个异步设计都建立在它那 9 条结论上。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch3>5. 与载体绑定的其它缺口\u003C\u002Fh3>\n\u003Cp>这些不是待办项，是载体选择的直接后果：\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Cstrong>无调试信息\u003C\u002Fstrong>：\u003Ccode>DynamicMethod\u003C\u002Fcode> 和动态程序集都出不了 PDB，调试器进不去脚本。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>不支持 NativeAOT\u003C\u002Fstrong>：异步载体依赖 \u003Ccode>Reflection.Emit\u003C\u002Fcode>。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>不支持类型声明与特性\u003C\u002Fstrong>：脚本编译成的是一个方法，不是一个编译单元;同步载体连承载新类型的模块都没有。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch3>6. 语言层面还没做的\u003C\u002Fh3>\n\u003Cp>\u003Ccode>yield\u003C\u002Fcode> 迭代器（runtime-async 只管 \u003Ccode>await\u003C\u002Fcode>，迭代器仍需自己写状态机）、\u003Ccode>await foreach\u003C\u002Fcode> \u002F \u003Ccode>await using\u003C\u002Fcode>、泛型局部函数、匿名类型、\u003Ccode>ref\u003C\u002Fcode> 局部与返回、\u003Ccode>Span&lt;T&gt;\u003C\u002Fcode> \u002F \u003Ccode>stackalloc\u003C\u002Fcode>、\u003Ccode>dynamic\u003C\u002Fcode>。\u003C\u002Fp>\n\u003Cp>完整清单在仓库 \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Ffs7744\u002FV.Script\u002Fblob\u002Fmain\u002Fdocs\u002Fdesign.md\" target=\"_blank\" rel=\"noopener noreferrer nofollow\">\u003Ccode>docs\u002Fdesign.md\u003C\u002Fcode>\u003C\u002Fa> 的「暂不支持」一节，每一项都写了为什么没做。\u003C\u002Fp>\n\u003Chr>\n\u003Ch2>结语\u003C\u002Fh2>\n\u003Cp>回到最初那个好奇：\u003Cstrong>动态生成的代码能不能借 runtime-async 拿到好性能？\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cp>答案是能。单次 \u003Ccode>await\u003C\u002Fcode> 25.9 ns vs 手写 12.4 ns，同步执行与手写 C# 持平，而发射端的 \u003Ccode>await\u003C\u002Fcode> 支持只有两行代码。runtime-async 确实把\"生成异步代码\"从一件需要重写编译器 lowering 的事，变成了一件发射一条 \u003Ccode>call\u003C\u002Fcode> 的事。\u003C\u002Fp>\n\u003Cp>但真正的收获不在这个结论，而在过程里那几处\u003Cstrong>意料之外的成本\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cul>\n \u003Cli>最贵的东西不是 runtime-async，是 \u003Ccode>DynamicMethod\u003C\u002Fcode> 少了一个 \u003Ccode>SetImplementationFlags\u003C\u002Fcode>——一个 API 缺口换来 40 倍编译开销和 31 KB 常驻内存。\u003C\u002Fli>\n \u003Cli>第二贵的东西是我自己加的超时机制，占了 90% 的运行时开销，删掉才看见真实数字。\u003C\u002Fli>\n \u003Cli>最危险的东西是 \u003Ccode>catch\u003C\u002Fcode> 里的 \u003Ccode>await\u003C\u002Fcode>——它会在测试里\"看起来能跑\"，然后在生产里带走整个进程。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>这三条都不是读文档能读出来的。\u003C\u002Fp>\n\u003Cp>代码、完整设计文档、性能基准和那个验证工具都在：\u003C\u002Fp>\n\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fgithub.com\u002Ffs7744\u002FV.Script\" target=\"_blank\" rel=\"noopener noreferrer nofollow\">https:\u002F\u002Fgithub.com\u002Ffs7744\u002FV.Script\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cp>如果你也在 .NET 上做代码生成，\u003Ca href=\"https:\u002F\u002Fgithub.com\u002Ffs7744\u002FV.Script\u002Ftree\u002Fmain\u002Ftools\u002FV.Script.RuntimeAsyncCheck\" target=\"_blank\" rel=\"noopener noreferrer nofollow\">\u003Ccode>tools\u002FV.Script.RuntimeAsyncCheck\u003C\u002Fcode>\u003C\u002Fa> 可能是最值得先跑一遍的东西——它会告诉你，在你手上这个运行时版本上，哪些前提还成立。\u003C\u002Fp>","起因：一个好奇 事情的开头很简单。 给 .NET 做过动态脚本的人大概都碰到过同一堵墙：表达式树也好、Reflection.Emit 也好，都很难支持 async\u002Fawait。原因不在语法，而在 await 的实现方式——C# 编译器要为每个 async 方法生成一个状态机结构体，把方法体切成若干片，配上 AsyncTaskMethodBuilder、awaiter 字段、MoveNext 的巨型 switch。这套东西你在编译期用 Roslyn 生成很自然，但要在运行时用 IL 一条条 emit 出来，工作量和出错面都大到不合理。 所以脚本引擎通常二选一：要么放弃 await，要么退回到解释执行（然后性能就没得谈了）。 后来看到 runtime-async 这个提案，我的第一反应是——如果状态机改由 JIT 来生成，那\"动态生成的代码\"和\"编译期生成的代码\"不就站在同一起跑线上了吗？ 换句话说：一个运行时 emit 出来的方法，能不能既真正支持 await，又保持接近手写 C# 的性能？ 这个好奇没法靠读文档回答，只能写代码试。于是有了 V.Script：一个编译成委托、兼容 C# 类型系统的轻量脚本引擎。 结论先放这里：能，但代价在你可能没预料到的地方。 关于版本，先说清楚 这个想法最早是冲着 .NET 10 去的——runtime-async 是在 .NET 10 周期里作为实验特性登场的。 但本文所有代码和所有数字，都是在 .NET 11.0.100-preview.7 上跑出来的，仓库的 global.json 也精确 pin 了这个预览版号。我没有在 .NET 10 上完整跑过下面这套验证，所以不替它背书；如果你在 .NET 10 上试，请自己先跑一遍仓库里的 tools\u002FV.Script.RuntimeAsyncCheck。 另外 AsyncHelpers 目前带着 SYSLIB5007 实验性标记，签名随时可能变。 runtime-async 是什么：一处 IL 层面的差异 先看传统的 async 方法在 IL 里长什么样。这是 async Task\u003Cint> F() => await GetAsync(); 编译出来的东西的骨架： \u002F\u002F 编译器额外生成的类型 .class nested private sealed beforefieldinit '\u003CF>d__0' extends [System.Runtime]System.ValueType implements [System.Runtime]IAsyncStateMachine { .field public int32 '\u003C>1__state' .field public valuetype AsyncTaskMethodBuilder`1\u003Cint32> '\u003C>t__builder' .field private valuetype TaskAwaiter`1\u003Cint32> '\u003C>u__1' .method private hidebysig newslot virtual final instance void MoveNext() cil managed { \u002F\u002F 一个按 \u003C>1__state 分派的大 switch， \u002F\u002F 把原方法体切成若干段，每段之间保存\u002F恢复局部变量 } } \u002F\u002F 原方法只剩下引导代码 .method public hidebysig static class Task`1\u003Cint32> F() cil managed { .custom instance void AsyncStateMachineAttribute::.ctor(class Type) \u002F\u002F 构造状态机、Start、返回 builder.Task } 要用 Reflection.Emit 复刻这一坨：你得定义一个额外的类型、实现 IAsyncStateMachine、自己做局部变量提升、自己切分基本块、自己写状态分派。这不是\"麻烦\"，这是重新实现一遍编译器里最难的一个 lowering。 runtime-async 把这件事整个搬走了。 同样的语义，IL 变成这样： .method public hidebysig static class Task`1\u003Cint32> Run(class ScriptHost, !!TGlobals) cil managed async \u002F\u002F ^^^^^ \u002F\u002F MethodImplAttributes.Async == 0x2000 { \u002F\u002F 直线 IL，没有状态机，没有额外类型 call class Task`1\u003Cint32> Service::GetAsync() call !!0 AsyncHelpers::Await\u003Cint32>(class Task`1\u003C!!0>) \u002F\u002F ← 挂起点 ret \u002F\u002F ← 注意这里 } 三处关键差异： 方法上多了一个 async 实现标志（MethodImplAttributes.Async，值 0x2000）。它告诉 JIT：这个方法体需要被改写成状态机。 await 变成一次普通的 call，调用 System.Runtime.CompilerServices.AsyncHelpers.Await\u003CT>。JIT 认得这个调用，把它变成真正的挂起点。 签名声明返回 Task\u003Cint>，但 IL 里 ret 弹出的是 int。包装成 Task 是运行时的事，emit 方不用管。 对一个代码生成器来说，这个差异是决定性的。在 V.Script 里，整个 await 的发射逻辑就是这么长： src\u002FV.Script\u002FEmit\u002FIlEmitter.Expressions.cs： \u002F\u002F\u002F \u003Csummary> \u002F\u002F\u002F A suspension point. The JIT turns this call into the state machine, so the whole of \u002F\u002F\u002F \u003Cc>await\u003C\u002Fc> support on the emit side is one operand plus one call. \u002F\u002F\u002F \u003C\u002Fsummary> private void EmitAwait(BoundAwait await) { EmitExpression(await.Operand); _il.Emit(OpCodes.Call, await.AwaitHelper); } 两行。 绑定器那边也只是根据操作数类型在 Task \u002F Task\u003CT> \u002F ValueTask \u002F ValueTask\u003CT> 四个重载里挑一个。原本\"重新实现一遍 lowering\"的工作量，压缩成了\"选个重载再 emit 一条 call\"。 但这里有一个坑：DynamicMethod 用不了 好消息说完了，说坏消息。 动态生成方法最轻的载体是 DynamicMethod：不需要程序集、不需要类型，委托没人引用时自动回收。V.Script 的同步脚本就用它——一个脚本大约 1.3 KB、几微秒。 问题是：DynamicMethod 没有 SetImplementationFlags。 tools\u002FV.Script.RuntimeAsyncCheck\u002FProgram.cs： Check(\"DynamicMethod 无法标记 Async\", typeof(DynamicMethod).GetMethod(\"SetImplementationFlags\") is null, \"这一个 API 缺口决定了异步脚本必须用独占程序集\"); 上面这行来自仓库里的 tools\u002FV.Script.RuntimeAsyncCheck——一个专门用来验证 runtime-async 前提条件的小程序。这一个 API 缺口，直接决定了整个异步方案的形状：异步脚本没法用 DynamicMethod，只能退到 AssemblyBuilder + MethodBuilder。 于是 V.Script 有了两个载体： 同步脚本 异步脚本 载体 DynamicMethod 独占的 collectible 程序集 内存 ~1.3 KB ~31 KB 回收 委托没人引用即回收 需要显式 Dispose，卸载整个程序集 能否标记 Async ❌ ✅ 这个\"一个 API 缺口 → 载体必须换 → 成本涨 24 倍\"的因果链，是这个项目里最贵的一课，后面性能一节还会再见到它。 顺带一提，async lambda 也踩同一个坑：lambda 平时编译成 DynamicMethod，一旦写了 async，它就必须搬进程序集。所以同步脚本里出现 async lambda，也会让这个脚本多出一个程序集（脚本体本身仍是 DynamicMethod，只有这些 lambda 搬家）。 长什么样 using var engine = new ScriptEngine(ScriptOptions.Default); \u002F\u002F 同步：编译成委托，DynamicMethod 载体 using var rule = engine.Compile\u003COrder, bool>( \"Total > 1000 && Customer.IsVip\"); bool ok = rule.Run(order); \u002F\u002F 异步：真正的 await，独占程序集载体 using var flow = engine.CompileAsync\u003CContext, int>(\"\"\" var total = 0; for (var i = 0; i \u003C Ids.Length; i++) total += await Service.GetAsync(Ids[i]); return total; \"\"\"); int result = await flow.RunAsync(context); Compile 和 CompileAsync 是两个不同的入口，不是一个方法加个开关——因为它们背后是两个载体、两套成本模型，让调用方知道自己在付什么代价比\"透明\"更重要。 性能实测 环境：Windows 11 x64，.NET 11.0.100-preview.7，BenchmarkDotNet 短任务、进程内 toolchain。 同步执行：与手写 C# 持平 场景 手写 C# 脚本 分配 decimal 公式 20.0 ns 21.0 ns 0 B bool 规则 5.2 ns 8.7 ns 0 B 1000 次循环 3100 ns 3077 ns 0 B 这个结果没什么好惊讶的：两侧都是 JIT 编译的 IL，持平才是预期。1000 次循环那行脚本略快是噪声，不必当真。 异步执行：多一次委托调用的量级 场景 手写 C# 脚本 单次 await 12.4 ns 25.9 ns 循环内 10 次 await 12.0 ns 24.6 ns 这是本文最想给出的那个数字。 一个运行时 emit 出来的异步方法，单次 await 是 25.9 ns——同一量级，不是\"慢一个数量级\"。 两个必须说明的点，否则这张表会骗人： 基线也开了 runtime-async=on 编译。 如果拿脚本的 runtime-async 去比手写 C# 的经典状态机，比的是两种 lowering，不是这个引擎做得好不好。 被 await 的 Task 都是已完成的，量的是 runtime-async 的快路径，不含调度开销。真挂起的场景由调度器主导，两边差异会被淹没。 还有一个数字值得单独讲：在移除超时机制之前，同一场景要 241 ns。 早期版本给每次异步调用都挂了 linked CancellationTokenSource + 定时器，光这套\"安全设施\"就吃掉了 90% 的时间。把它整个删掉、把取消交还给宿主之后才有上面的 25.9 ns。异步的开销往往不在 await 本身，而在你顺手加在它周围的东西。 编译：代价全在这里 场景 耗时 同步，小脚本 8.6 µs 同步，中等（5 条语句） 31.0 µs 异步，小脚本 346 µs 异步，含 await 的循环 629 µs 缓存命中 54.5 ns 异步比同步贵约 40 倍，而且这 40 倍跟 runtime-async 一点关系都没有——它全部来自 collectible 程序集的创建与卸载。也就是说： DynamicMethod 不能标记 Async 这一个 API 缺口，全部代价就体现在这张表上。 如果哪天 DynamicMethod 补上 SetImplementationFlags，异步脚本的编译开销会直接掉回同步那一档，31 KB 的常驻内存也一起消失。 局限：请务必读完这一节 前面都是好消息，这一节是买单的地方。 1. await 不能出现在 catch \u002F finally 里——这会让进程直接崩溃 这是最严重的一条。当前运行时对处理器块内的挂起点不提供保护：不是抛异常，不是返回错误，是整个进程带着 0xC0000005 退出。 验证工具里这条 case 必须开子进程来跑，因为它没法在本进程里安全地测： OK catch 内真正挂起的 await 会终止进程（预期如此） 子进程退出码 -1073741819；为 0 说明运行时行为已改变，需重新评估 VS3004 所以 V.Script 的绑定器在编译期无条件拒绝这种写法（诊断码 VS3004），不提供任何开关。 这里有个反直觉的细节值得单独说：AsyncHelpers.Await 对已完成的 Task 走快路径，根本不会到达挂起点。所以你在 catch 里写 await SomethingAlreadyCompleted() 很可能跑得好好的——直到某天那个 Task 真的挂起，然后线上进程没了。正因为它\"偶尔能跑通\"，编译期拒绝才更有必要，而不是更没必要。 2. 异步脚本 31 KB 常驻，且必须显式释放 每个异步脚本一个 collectible 程序集。一万个异步脚本 ≈ 310 MB，而且只有在你 Dispose 掉脚本、且没有任何委托还引用着它时才会卸载。 热更新场景要按代次换新，不能无限编译新版本。同步脚本没有这个问题。 3. 生成的程序集失去 skipVisibility DynamicMethod 可以用 skipVisibility: true 访问 globals 类型的非公开成员;独立程序集不行。 所以异步脚本（以及带 async lambda 的同步脚本）只能访问公开成员。给 lambda 加个 async 会改变它的可见性权限——这个副作用不明显，但确实存在。 4. 预览版依赖 AsyncHelpers 带 SYSLIB5007 实验性标记，签名可能变。V.Script 把使用点全部收拢在 Binding\u002FAwaitHelpers.cs 一个文件里，就是为了将来好改。 global.json 精确 pin 了预览版号（rollForward 没法从正式版号回退到预览版），GA 后需要改。 GA 后请重跑 tools\u002FV.Script.RuntimeAsyncCheck，整个异步设计都建立在它那 9 条结论上。 5. 与载体绑定的其它缺口 这些不是待办项，是载体选择的直接后果： 无调试信息：DynamicMethod 和动态程序集都出不了 PDB，调试器进不去脚本。 不支持 NativeAOT：异步载体依赖 Reflection.Emit。 不支持类型声明与特性：脚本编译成的是一个方法，不是一个编译单元;同步载体连承载新类型的模块都没有。 6. 语言层面还没做的 yield 迭代器（runtime-async 只管 await，迭代器仍需自己写状态机）、await foreach \u002F await using、泛型局部函数、匿名类型、ref 局部与返回、Span\u003CT> \u002F stackalloc、dynamic。 完整清单在仓库 docs\u002Fdesign.md 的「暂不支持」一节，每一项都写了为什么没做。 结语 回到最初那个好奇：动态生成的代码能不能借 runtime-async 拿到好性能？ 答案是能。单次 await 25.9 ns vs 手写 12.4 ns，同步执行与手写 C# 持平，而发射端的 await 支持只有两行代码。runtime-async 确实把\"生成异步代码\"从一件需要重写编译器 lowering 的事，变成了一件发射一条 call 的事。 但真正的收获不在这个结论，而在过程里那几处意料之外的成本： 最贵的东西不是 runtime-async，是 DynamicMethod 少了一个 SetImplementationFlags——一个 API 缺口换来 40 倍编译开销和 31 KB 常驻内存。 第二贵的东西是我自己加的超时机制，占了 90% 的运行时开销，删掉才看见真实数字。 最危险的东西是 catch 里的 await——它会在测试里\"看起来能跑\"，然后在生产里带走整个进程。 这三条都不是读文档能读出来的。 代码、完整设计文档、性能基准和那个验证工具都在： https:\u002F\u002Fgithub.com\u002Ffs7744\u002FV.Script 如果你也在 .NET 上做代码生成，tools\u002FV.Script.RuntimeAsyncCheck 可能是最值得先跑一遍的东西——它会告诉你，在你手上这个运行时版本上，哪些前提还成立。",6538,{"id":6,"kind":7,"title":11,"summary":13,"image":14,"href":15,"meta":36,"badge":10,"author":12,"stats":-1,"accent":37,"coverRatio":38,"tags":39},"2026 · 软件开发","#2563eb","16 \u002F 10",[18],{"targetType":8,"targetId":9,"likedByMe":41,"likeCount":42,"commentCount":42,"contentLikeCount":42,"contentCommentCount":42,"sourceLikeCount":42,"sourceCommentCount":42},false,0,[44,51,57,63,70,77,84,90],{"id":45,"kind":7,"title":46,"summary":47,"image":48,"href":49,"meta":36,"badge":10,"author":12,"stats":-1,"accent":37,"coverRatio":38,"tags":50},"NEWS_ARTICLE:855","被罚了500后，整个人都变老实了","我们组有个同事，以前是那种闲不住的人。现在也闲不住 只不过是不敢动了。 去年部门空降了一位新总监，阿里P8，第一次全员会PPT上就八个大字：拥抱变化，永不言弃。 新领导上来搞流程改革，每个模块指定Owner，出事追责到人，A级事故罚款500起步，B级300 依次类推，发版上线改配置全走审批。说实话之","https:\u002F\u002Fimg2024.cnblogs.com\u002Fblog\u002F273387\u002F202608\u002F273387-20260818222617092-925481998.png","\u002Fnews\u002F855",[18],{"id":52,"kind":7,"title":53,"summary":54,"image":14,"href":55,"meta":36,"badge":10,"author":12,"stats":-1,"accent":37,"coverRatio":38,"tags":56},"NEWS_ARTICLE:862","go中make声明切片, 修改切片,append给切片扩容,合并切片,复制切片","make 声明切片 make([]类型, 长度， 容量) kage main import (&quot;fmt&quot;) \u002F\u002F 入口函数 func main() { \u002F\u002F make 声明切片，长度是4， 容量是10 var sliceArr1 = make([]int, 4, 10) fmt.","\u002Fnews\u002F862",[18],{"id":58,"kind":7,"title":59,"summary":60,"image":14,"href":61,"meta":36,"badge":10,"author":12,"stats":-1,"accent":37,"coverRatio":38,"tags":62},"NEWS_ARTICLE:866","Spring Boot事件监听，这东西到底解决啥问题？","一、先聊个场景，你就明白这玩意儿干啥的了 点过外卖吧？那咱们就用这个场景来说事。 你掏出手机下了单，付了钱。接下来会发生啥？ 厨房那头开始备菜炒菜（这件事耽误不得，客户饿着呢） 手机收到一条短信：&quot;您的订单已收到，预计30分钟送达&quot; 你的会员账户里多了一堆积分 店长那个收银小喇叭","\u002Fnews\u002F866",[18],{"id":64,"kind":7,"title":65,"summary":66,"image":67,"href":68,"meta":36,"badge":10,"author":12,"stats":-1,"accent":37,"coverRatio":38,"tags":69},"NEWS_ARTICLE:881","RSA 密码传输加密通用接入方案","@目录前言一、介绍二、RSA 在本方案中的角色三、密文协议格式四、密钥生成与 Apollo 配置4.1 编译工具类4.2 执行生成密钥4.3 执行输出示例4.4 Apollo 配置示例五、后端接入方式AOP 注解示例六、后端解密流程七、前端接入方式7.1 获取公钥和 nonce7.2 使用 RSA-","https:\u002F\u002Fimg2024.cnblogs.com\u002Fblog\u002F1867541\u002F202608\u002F1867541-20260831165110853-162420906.png","\u002Fnews\u002F881",[18],{"id":71,"kind":7,"title":72,"summary":73,"image":74,"href":75,"meta":36,"badge":10,"author":12,"stats":-1,"accent":37,"coverRatio":38,"tags":76},"NEWS_ARTICLE:888","[开源] LogCrate：免解压、支持筛选和 AI 分析的 PC 端桌面日志工具","日常排查客户软件运行问题时，经常需要反复下载日志、解压缩，再从一堆文件里慢慢翻找，过程比较麻烦；而且很多日志工具也不支持针对具体字段进行筛选。 对比了一些市面上的日志分析工具后，发现能够同时兼容归档阅读、目录监控、到达通知、结构化筛选和 AI 分析的工具并不多，于是就自己动手做了一款。 PS：自己做","https:\u002F\u002Fiili.io\u002FCy4ztaa.png","\u002Fnews\u002F888",[18],{"id":78,"kind":7,"title":79,"summary":80,"image":81,"href":82,"meta":36,"badge":10,"author":12,"stats":-1,"accent":37,"coverRatio":38,"tags":83},"NEWS_ARTICLE:900","java 多线程开发系列之七：玩转多线程（线程的协作）","线程的协作多种多样，这次主要说说wait和notify两种Object的方法。这也是java早期原生的重要的协作机制之一。先说下这两个英文单词：wait [weɪt] v.等待;等候;(尤指长期地)希望，盼望，期待notify [ˈnəʊtɪfaɪ] vt.通知;(正式)通报;也就是一个等待，一个通","https:\u002F\u002Fimg2024.cnblogs.com\u002Fblog\u002F704073\u002F202608\u002F704073-20260831103138251-1707185366.png","\u002Fnews\u002F900",[18],{"id":85,"kind":7,"title":86,"summary":87,"image":14,"href":88,"meta":36,"badge":10,"author":12,"stats":-1,"accent":37,"coverRatio":38,"tags":89},"NEWS_ARTICLE:902","DeepSeek Harness 插件","准备环境 dsh 从源码运行，见 dsh 说明。或见之前分享的 DeepSeek Harness 开始。 开发插件 依照 dsh 文档，动手做一遍： 第一个插件: https:\u002F\u002Fdeepseek-harness.github.io\u002Fdeepseek-harness\u002Fdevelop\u002Fbasic\u002F 第","\u002Fnews\u002F902",[18],{"id":91,"kind":7,"title":92,"summary":93,"image":94,"href":95,"meta":36,"badge":10,"author":12,"stats":-1,"accent":37,"coverRatio":38,"tags":96},"NEWS_ARTICLE:917","[python] pywinauto使用指北","当业务系统没有API、命令行接口或可直接集成的数据通道时，桌面自动化往往是打通业务流程的最后一公里。pywinauto库通过Win32 API与Microsoft UI Automation（UIA）访问Windows窗口及控件，使Python脚本能够驱动桌面应用。本文聚焦于pywinauto的基础","https:\u002F\u002Fgitlab.com\u002Fluohenyueji\u002Farticle_picture_warehouse\u002F-\u002Fraw\u002Fmain\u002FCSDN\u002F%5Bpython%5D%20pywinauto%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8C%97\u002Fimgs\u002F1.jpg","\u002Fnews\u002F917",[18]]