惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - Franky
IT之家
IT之家
酷 壳 – CoolShell
酷 壳 – CoolShell
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
C
Check Point Blog
月光博客
月光博客
Jina AI
Jina AI
S
SegmentFault 最新的问题
WordPress大学
WordPress大学
博客园 - 叶小钗
人人都是产品经理
人人都是产品经理
博客园 - 司徒正美
腾讯CDC
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
有赞技术团队
有赞技术团队
博客园 - 聂微东
Hugging Face - Blog
Hugging Face - Blog
小众软件
小众软件
Last Week in AI
Last Week in AI
V
V2EX
罗磊的独立博客
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
阮一峰的网络日志
阮一峰的网络日志
NISL@THU
NISL@THU
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Threat Research - Cisco Blogs
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
L
Lohrmann on Cybersecurity
宝玉的分享
宝玉的分享
爱范儿
爱范儿
T
Troy Hunt's Blog
量子位
Project Zero
Project Zero
S
Secure Thoughts
V
Visual Studio Blog
美团技术团队
AI
AI
博客园 - 【当耐特】
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
The Last Watchdog
The Last Watchdog
V
Vulnerabilities – Threatpost
P
Privacy International News Feed
Spread Privacy
Spread Privacy
Schneier on Security
Schneier on Security
博客园_首页
P
Privacy & Cybersecurity Law Blog
博客园 - 三生石上(FineUI控件)
Apple Machine Learning Research
Apple Machine Learning Research
N
News | PayPal Newsroom

博客园 - Charltsing

C# 预处理器指令语言 - 完整语法速查 LoggerMessage:编写高性能的 .NET 日志 PDFImageViewer v2.1 PDF查看图像和无损导出工具,一键转换单图PDF为JBig2黑白PDF。兼谈PDF内嵌图像。 PDF的色彩空间 25 个使用.NET 10 的性能技巧 PDFontFixer v1.5 免费版,解决PDF文档复制粘贴乱码的问题,修复PDF文档字体的Unicode映射。 为什么 PDF 编辑这么难? Unicode、UTF-8、UTF-16 如何直接编辑Github的Readme.md文件 自动导入工程项目属性,Directory.Build.props 和 Directory.Build.targets 新版 C# 高效率编程指南 C# 内联数组(Inline Array):高性能数组的新选择 如何在 .NET 中使用 SIMD C# 中的安全零拷贝 使用UnsafeAccessor 访问私有字段 C# 12与.NET 8实战指南:20个提升代码质量的最佳实践 C# Net9的模块初始化器(Module Initializer) ECMA-335 CLI 规范附录 C#中避免GC压力和提高性能的8种技术 函数内联 硬件内在函数 官方:oPDF v2.1免费版,专业的PDF水印分析处理工具,无损去除水印,通杀八类PDF水印。它是PDFCommander 万能PDF水印删除工具的升级版。 C#自动引用Debug | Release版本的dll
.NET异步编程进阶:从语法糖到高性能架构的核心突破
Charltsing · 2025-12-07 · via 博客园 - Charltsing

.NET异步编程进阶:从语法糖到高性能架构的核心突破

在每个.NET开发者的职业生涯中,都会有一个转折点——异步不再只是"那个带await的奇怪东西",而成为你思考性能的核心部分。你不再将async视为语法糖,而是开始将其看作编排工具。你不会仅仅因为能够阻塞线程就去这样做。最终,你会明白编写高效的异步代码关乎意图,而非习惯。这篇文章正是关于这种转变,这种将经验丰富的开发者与那些到处撒await并期待最佳结果的人区分开来的转变。我们将深入探讨如何智能地结合I/O和CPU工作、避免隐藏的线程池成本,以及使用新的.NET功能使异步比以往更安全、更快速。

1. 使用管道或通道组合异步I/O和CPU密集型任务
异步和CPU密集型工作并不能开箱即用地良好配合。你可以异步从文件读取数据,但一旦开始在同一线程上处理它,你的异步优势就消失了。这就是为什么高级开发者会将I/O密集型与CPU密集型工作负载分开。假设你正在从套接字流式传输数据或逐块读取大文件。初学者可能会这样做:

await foreach (var chunk in ReadFileAsync("data.log"))
{
    ProcessChunk(chunk); // CPU密集型
}

这看起来不错,但它将读取和处理都序列化在相同的异步流中。每个块都要等待前一个完成。

更好的方法?使用System.Threading.Channels或Pipelines来分离关注点。

var channel = Channel.CreateBounded<byte[]>(new BoundedChannelOptions(100)
{
    SingleWriter = true,
    SingleReader = true
});

// 生产者
_ = Task.Run(async () =>
{
    awaitforeach (var chunk in ReadFileAsync("data.log"))
    await channel.Writer.WriteAsync(chunk);
    channel.Writer.Complete();
});

// 消费者
awaitforeach (var chunk in channel.Reader.ReadAllAsync())
{
    ProcessChunk(chunk);
}

现在你已经分离了生产者(I/O密集型)和消费者(CPU密集型)任务。读取不会阻塞处理,反之亦然。你仍然是异步的,但现在你的代码可以在不增加复杂性的情况下使用更多可用资源。

为何重要:这种模式让你的I/O保持异步,而不会因CPU工作而减慢。这是.NET中高性能服务器和流处理器大规模处理并发的方式。

2. 避免过度延续;在热异步路径中优先使用内联完成
异步方法创建延续——在await之后运行的逻辑。大多数情况下这没问题,但在热路径(如紧密循环或性能关键的异步方法)中,不必要的延续会增加开销。

考虑这个:

for (int i = 0; i < 1000; i++)
{
    await DoWorkAsync();
}

每次迭代都会引入一个延续,这意味着上下文捕获、排队和潜在的线程池切换。

更聪明的方法是检查任务是否已经完成,如果是,则完全跳过延续。JIT和编译器可以很好地优化这种模式:

var task = DoWorkAsync();

if (task.IsCompletedSuccessfully)
{
    // 内联快速路径
    HandleResult(task.Result);
}
else
{
    // 回退到异步
    await task;
}

你会在.NET内部、ASP.NET Core等库甚至Task.WhenAll中看到这种模式。这不是要对所有东西进行微优化,而是要知道异步开销何时重要。

为何重要:当你构建延迟敏感的API时,每个延续都是潜在的延迟。内联完成让你的热路径保持真正的"热"。

3. 在.NET 8中使用Task.ConfigureAwaitOptions.SuppressThrowing实现更安全的await
这是.NET 8的新功能。通常,当任务失败时,await会重新抛出其异常。这通常是你想要的,但在底层库代码中,这可能浪费或不安全。

在.NET 8中,你现在可以在await期间抑制异常抛出:

await task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
然后在之后手动检查任务:

if (task.IsFaulted)
{
    Log(task.Exception);
}

这避免了正常异常重新抛出带来的额外分配和堆栈展开成本。在预期异常并手动处理的低级框架、库或日志子系统中特别有用。

为何重要:异常处理是昂贵的,并非每个await都需要重新抛出。这个功能让你可以精细控制异常的流动方式,而不会失去异步安全性。

4. 在链式返回任务的task时使用Task.Unwrap()
你可能之前见过甚至在不经意间写过这个:

await Task.Run(async () =>
{
    await DoSomethingAsync();
});

这是一个返回另一个任务的任务。没有Unwrap,你最终会得到Task,每个额外层都是堆上的另一个对象。

与其编写尴尬的双重await,不如使用Task.Unwrap()展平嵌套任务:

var innerTask = Task.Run(() => DoSomethingAsync());
await innerTask.Unwrap();

这使得意图明确:你在说"运行这个异步任务并将其完成作为单个操作来await"。

为何重要:你减少了分配,简化了异步链,并使控制流更清晰。这也是库在编排管道内部展平复杂异步调用的方式。

5. 为分离的工作负载显式使用TaskScheduler.Default
当你在不指定调度程序的情况下排队任务时,它会使用当前的同步上下文,这可能是你的UI线程或ASP.NET请求上下文。这并不总是你想要的。

例如,在UI或Web应用中:

await Task.Run(() => DoHeavyWork());

这可能仍然捕获同步上下文,导致不必要地封送回UI线程。

如果你真正想要一个分离的后台工作负载,请显式指定:

await Task.Factory.StartNew(
() => DoHeavyWork(),
CancellationToken.None,
TaskCreationOptions.DenyChildAttach,
TaskScheduler.Default);

现在你的任务在默认线程池中运行,完全脱离任何同步上下文,非常适合后台处理或低优先级作业。

为何重要:显式调度程序给你可预测的行为,并保持异步流清洁。这也是"它能工作"和"它能扩展"之间的微妙区别之一。

异步编程是这样一个领域

你越深入,就越意识到自己实际拥有多少控制权。你开始看到管道、调度程序和延续如何在底层交互,这种意识让你能够编写更快、更清晰、更有弹性的代码。事实是,异步精通不是到处抛await。而是理解它属于哪里,不属于哪里,以及如何设计在负载下保持高效的系统。所以下次你使用await时,想想幕后真正发生了什么。你是否不必要地链式任务?捕获了你不需要的上下文?还是将I/O和CPU阻塞在一起?这些小的决定会累积起来,而这正是将高级工程师与其他所有人区分开来的地方。