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

推荐订阅源

T
The Blog of Author Tim Ferriss
S
Schneier on Security
H
Help Net Security
aimingoo的专栏
aimingoo的专栏
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
H
Hackread – Cybersecurity News, Data Breaches, AI and More
MongoDB | Blog
MongoDB | Blog
云风的 BLOG
云风的 BLOG
H
Hacker News: Front Page
C
Check Point Blog
P
Privacy International News Feed
IT之家
IT之家
爱范儿
爱范儿
AWS News Blog
AWS News Blog
The Hacker News
The Hacker News
Stack Overflow Blog
Stack Overflow Blog
Project Zero
Project Zero
Microsoft Azure Blog
Microsoft Azure Blog
量子位
The Cloudflare Blog
P
Proofpoint News Feed
Hugging Face - Blog
Hugging Face - Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
C
Cybersecurity and Infrastructure Security Agency CISA
I
Intezer
Martin Fowler
Martin Fowler
Scott Helme
Scott Helme
酷 壳 – CoolShell
酷 壳 – CoolShell
A
Arctic Wolf
T
Threat Research - Cisco Blogs
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
T
The Exploit Database - CXSecurity.com
B
Blog
Simon Willison's Weblog
Simon Willison's Weblog
U
Unit 42
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Microsoft Security Blog
Microsoft Security Blog
博客园_首页
J
Java Code Geeks
K
Kaspersky official blog
Webroot Blog
Webroot Blog
C
CERT Recently Published Vulnerability Notes
H
Heimdal Security Blog
G
Google Developers Blog
T
Tor Project blog
W
WeLiveSecurity
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Blog — PlanetScale
Blog — PlanetScale
小众软件
小众软件
有赞技术团队
有赞技术团队

博客园 - 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阻塞在一起?这些小的决定会累积起来,而这正是将高级工程师与其他所有人区分开来的地方。