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

推荐订阅源

量子位
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
F
Fortinet All Blogs
博客园 - 聂微东
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Hugging Face - Blog
Hugging Face - Blog
V
Visual Studio Blog
小众软件
小众软件
有赞技术团队
有赞技术团队
雷峰网
雷峰网
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
AWS News Blog
AWS News Blog
C
Cisco Blogs
美团技术团队
T
Threat Research - Cisco Blogs
C
CERT Recently Published Vulnerability Notes
人人都是产品经理
人人都是产品经理
宝玉的分享
宝玉的分享
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
酷 壳 – CoolShell
酷 壳 – CoolShell
Stack Overflow Blog
Stack Overflow Blog
W
WeLiveSecurity
D
DataBreaches.Net
博客园 - 司徒正美
Blog — PlanetScale
Blog — PlanetScale
IT之家
IT之家
云风的 BLOG
云风的 BLOG
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Simon Willison's Weblog
Simon Willison's Weblog
Google DeepMind News
Google DeepMind News
T
The Blog of Author Tim Ferriss
Know Your Adversary
Know Your Adversary
NISL@THU
NISL@THU
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
The Cloudflare Blog
Vercel News
Vercel News
月光博客
月光博客
T
Tailwind CSS Blog
H
Help Net Security
aimingoo的专栏
aimingoo的专栏
P
Proofpoint News Feed
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Spread Privacy
Spread Privacy
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Cisco Talos Blog
Cisco Talos Blog
Microsoft Security Blog
Microsoft Security Blog
V
V2EX
WordPress大学
WordPress大学
Cyberwarzone
Cyberwarzone
Recent Announcements
Recent Announcements

博客园 - 邓磊Lei

为什么 Go 一个 HTTP 服务可以同时处理数万连接 gin怎么处理一次http请求的 我的经验:git提交信息分别用什么icon C# 也能像 Python 一样写脚本 | .NET 10 构建基于文件的应用 .NET 10 使用 Microsoft.AspNetCore.OpenApi 实现 API 版本管理 ASP.NET Core 内存缓存实战:一篇搞懂该怎么配、怎么避坑 Microsoft Agent Framework + Kimi API 实战:控制台应用跑通单次与多轮 Agent 对话 开发实战:asp.net core + ef core 实现动态可扩展的分页方案 聊聊 ASP.NET Core 中间件和过滤器的区别 Python 入门:从“其他语言”到 Pythonic 思维的完整迁移手册 Redis:延迟双删的适用边界与落地细节 Serilog:从结构化日志认知到 .NET 工程落地 EF Core 原生 SQL 实战:FromSql、SqlQuery 与对象映射边界 EF Core 拦截器实战:SaveChangesInterceptor、CommandInterceptor 与审计落地 ASP.NET Core 外部依赖调用治理实战:HttpClientFactory、Polly 与幂等边界 .NET .Result 避坑指南:不同框架下的死锁与线程池饥饿 EF Core 慢查询排查实战:TagWith、OpenTelemetry、执行计划,30 分钟定位性能瓶颈 EF Core 并发冲突实战:乐观锁、RowVersion 与 DbUpdateConcurrencyException 怎么处理 EF Core 写入链路深拆:从 ChangeTracker 到 SQL Batch 的性能诊断与优化 ASP.NET Core 认证鉴权实战:JWT、Policy 与权限边界怎么落地 C# 异步编程深水区:Task、ValueTask、线程池饥饿与背压设计 EF Core 查询性能黑洞:Include、投影与跟踪策略的边界 从 IApplicationBuilder 到 RequestDelegate:ASP.NET Core 请求管线的性能与可观测性实战 如何在 rider 中配置 SonarQube
.NET 进阶之路:异步、并发与内存管理的系统性认知
邓磊Lei · 2026-03-31 · via 博客园 - 邓磊Lei

异步编程模式的演进与 TAP 最佳实践

.NET 的异步编程经历了三个时代。理解这段历史不是为了考古,而是因为你在维护老代码时必然会遭遇它们,理解它们才能优雅地迁移。

模式 时代 标志 状态
APM(异步编程模型) .NET 1.x BeginXxx / EndXxx 已淘汰
EAP(基于事件的异步) .NET 2.0 XxxAsync + XxxCompleted 事件 遗留代码
TAP(基于任务的异步) .NET 4.0+ Task / async / await 推荐使用

TAP 方法的命名与签名规范

很多人写异步方法时忽视规范,导致 API 设计混乱。TAP 有一套严格的约定:

// ✅ 标准命名:方法名 + Async 后缀
public Task<int> ReadAsync(byte[] buffer, int offset, int count);

// ✅ 已有同名 EAP 方法时,用 TaskAsync 后缀
public Task<string> GetTaskAsync(string url);

// ✅ 返回 void 的同步对应版本 → 返回 Task
public Task SaveAsync(string path);

// ✅ 返回 T 的同步对应版本 → 返回 Task<T>
public Task<UserDto> GetUserAsync(int userId);

// ❌ 避免:out/ref 参数在 TAP 中禁止使用
// 应将多返回值包装为 tuple 或自定义类型
public Task<(bool Success, string Error)> TryParseAsync(string input);

Task 的生命周期:一个经常被忽视的细节

Task冷任务(Cold Task)热任务(Hot Task) 之分。new Task(...) 创建的是冷任务,需要手动调用 Start()。但 TAP 方法返回的 Task 必须是已激活的热任务——调用者不应该也不需要调用 Start()

⚠️ 常见错误

如果你在 TAP 方法内部通过 new Task() 构造任务后忘记调用 Start() 就返回它,调用者会陷入永久等待。始终确保返回的 Task 已处于运行状态。

异常处理的正确姿势

异步方法中的异常处理有一个重要原则:参数验证异常应该在 async 方法外层同步抛出,这样调用者能立即捕获,而不必 await 后才能发现错误。

// ✅ 推荐:参数验证在外层同步完成
public Task<int> ProcessAsync(string input)
{
    if (input == null)
        throw new ArgumentNullException(nameof(input)); // 同步抛出

    return ProcessCoreAsync(input); // 委托给真正的 async 方法
}

private async Task<int> ProcessCoreAsync(string input)
{
    // 真正的异步工作
    var result = await DoWorkAsync(input);
    return result;
}

取消令牌与进度报告:让异步操作可控

写了两三年 .NET,你可能已经在用 CancellationToken,但真正理解它的状态机和设计模式的人并不多。

CancellationToken 的三种终态

          Task 状态机:
Created ──Start()──▶ Running
│
┌─────────────┼─────────────┐
▼             ▼             ▼
Canceled Faulted RanToCompletion
(取消请求) (未处理异常) (正常完成)
│             │             │
└─────────────┴─────────────┘
       IsCompleted = true

取消时 Task 进入 Canceled 状态,IsCompleted 返回 true,但 await 它会抛出 OperationCanceledException

最佳实践:在计算密集型任务中轮询取消

internal Task<Bitmap> RenderAsync(
    ImageData data, CancellationToken cancellationToken)
{
    return Task.Run(() =>
    {
        var bmp = new Bitmap(data.Width, data.Height);
        for (int y = 0; y < data.Height; y++)
        {
            // 每行检查一次取消请求,不要每像素都检查(性能损耗)
            cancellationToken.ThrowIfCancellationRequested();

            for (int x = 0; x < data.Width; x++)
            {
                // 渲染像素 [x, y]
            }
        }
        return bmp;
    }, cancellationToken); // 传入 token 以便 Task 启动前就取消
}

进度报告:IProgress 的正确用法

不要用事件或回调来报告进度——IProgress<T> 是官方推荐的模式,它能自动处理线程同步问题(回调总是在创建 Progress<T> 实例的同步上下文中执行,通常是 UI 线程)。

// 定义时接受 IProgress<T> 参数
public async Task<string[]> FindFilesAsync(
    string pattern,
    CancellationToken ct = default,
    IProgress<int> progress = null) // 允许为 null
{
    var results = new List<string>();
    int count = 0;
    await foreach (var file in EnumerateFilesAsync(pattern, ct))
    {
        results.Add(file);
        progress?.Report(++count); // null 安全调用
    }
    return results.ToArray();
}

// 调用端:Progress<T> 捕获 UI 线程的同步上下文
var reporter = new Progress<int>(count =>
    progressBar.Value = count); // 这里可以安全更新 UI

await FindFilesAsync("*.cs", ct, reporter);

💡 设计建议

如果某个方法不支持取消,不要提供接受 CancellationToken 的重载——这会误导调用者。反之,如果支持取消,应当始终提供带 token 的重载。

任务并行库(TPL)与 Parallel 编程

并行编程最大的陷阱是分不清 CPU 密集型I/O 密集型 任务,用错了工具反而更慢。

📌 核心原则

CPU 密集型Task.Run() 分发到线程池;I/O 密集型async/await + TaskCompletionSource,不应绑定线程。

父子任务与 DenyChildAttach

使用第三方库时,如果对方内部用 TaskCreationOptions.AttachedToParent 创建任务,会导致你的父任务必须等待所有子任务完成——即使你不需要这种行为。使用 DenyChildAttach 可以隔离这种副作用。

// ❌ 默认行为:第三方 Widget 内部创建的子任务会延迟父任务完成
Task<Task> runWidget = Task.Factory.StartNew(
    () => thirdPartyWidget.Run());  // 子任务 sleep 5s,父任务也等 5s

// ✅ 正确做法:DenyChildAttach 隔离第三方任务
Task<Task> runWidget = Task.Factory.StartNew(
    () => thirdPartyWidget.Run(),
    TaskCreationOptions.DenyChildAttach);  // 父任务立即完成

Task.WhenAll vs Task.WhenAny 的适用场景

// WhenAll:等待所有任务完成(并行 I/O 的核心武器)
var tasks = urls.Select(url => httpClient.GetStringAsync(url));
string[] results = await Task.WhenAll(tasks);

// WhenAny:哪个先完成用哪个(超时控制的经典写法)
var dataTask = FetchDataAsync();
var timeout = Task.Delay(TimeSpan.FromSeconds(5));

var winner = await Task.WhenAny(dataTask, timeout);
if (winner == timeout)
    throw new TimeoutException("请求超时");

return await dataTask; // 再次 await 以解包异常

Task.FromResult 优化缓存命中

当异步方法能从缓存直接返回时,创建真正的异步操作是不必要的开销。Task.FromResult() 返回一个已完成的任务,零开销。

private static readonly ConcurrentDictionary<string, string>
    _cache = new();

public static Task<string> DownloadStringAsync(string url)
{
    // 缓存命中:返回已完成的 Task,无线程切换开销
    if (_cache.TryGetValue(url, out string? cached))
        return Task.FromResult(cached);

    // 缓存未命中:真正的异步下载
    return Task.Run(async () =>
    {
        var content = await _httpClient.GetStringAsync(url);
        _cache.TryAdd(url, content);
        return content;
    });
}

线程安全集合的选型与陷阱

System.Collections.Concurrent 命名空间提供了几个高性能线程安全集合,但选错了反而不如加锁的普通集合性能好。

集合类型 适用场景 注意事项
ConcurrentDictionary 多线程频繁读写键值对 GetOrAdd / AddOrUpdate 非原子操作
ConcurrentQueue FIFO 生产者-消费者场景 枚举不保证顺序稳定
BlockingCollection 有界缓冲 + 阻塞语义 需要配合 CompleteAdding() 正确关闭
ConcurrentBag 混合生产者-消费者(同线程添加取出) 纯生产消费场景比其他集合慢

ConcurrentDictionary 的非原子陷阱

这是很多人犯错的地方:ConcurrentDictionary 的所有单个方法是线程安全的,但复合操作("检查-然后-添加")不是原子的。

// ⚠️ 注意:valueFactory 可能被多个线程调用
// 但只有一个线程的结果会被保留
var value = dict.GetOrAdd(key, k =>
{
    // 这里的代码可能被并发执行多次!
    // 如果 factory 有副作用(如 DB 写入),需要额外处理
    return new ExpensiveObject(k);
});

// ✅ 如果 factory 有副作用,使用 Lazy<T> 确保只执行一次
var lazy = dict.GetOrAdd(key, k =>
    new Lazy<ExpensiveObject>(() => new ExpensiveObject(k)));
var obj = lazy.Value; // 真正的构造只发生一次

BlockingCollection:生产者-消费者管道

var queue = new BlockingCollection<WorkItem>(boundedCapacity: 100);

// 生产者
Task producer = Task.Run(() =>
{
    foreach (var item in GetWorkItems())
        queue.Add(item);

    queue.CompleteAdding(); // ⚠️ 必须调用!否则消费者永远阻塞
});

// 消费者:GetConsumingEnumerable 会在 CompleteAdding 后自动退出
Task consumer = Task.Run(() =>
{
    foreach (var item in queue.GetConsumingEnumerable())
        ProcessItem(item);
});

await Task.WhenAll(producer, consumer);

P/Invoke 与 Native 互操作

P/Invoke 是调用 Windows API 或 C 库的标准方式,但很多 .NET 开发者很少接触。理解它的基本原理能帮你在需要时快速上手,也能读懂底层库的代码。

最小化示例:调用 Windows API

using System.Runtime.InteropServices;

public static class NativeMethods
{
    // DllImport 声明:映射到 kernel32.dll 中的函数
    [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
    public static extern bool CreateDirectory(
        string lpPathName,
        IntPtr lpSecurityAttributes);

    // 现代写法(.NET 7+):LibraryImport + Source Generator(更快,AOT 友好)
    [LibraryImport("kernel32.dll", SetLastError = true,
        StringMarshalling = StringMarshalling.Utf16)]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static partial bool CreateDirectoryModern(
        string lpPathName,
        IntPtr lpSecurityAttributes);
}

最危险的陷阱:委托被 GC 回收

将委托转换为函数指针传给 Native 代码后,.NET GC 不知道 Native 代码还在使用这个指针。如果委托对象被回收,程序会崩溃。

// ❌ 危险:委托可能在 Native 调用期间被回收
NativeMethods.RegisterCallback(
    Marshal.GetFunctionPointerForDelegate(
        new MyCallback(OnEvent))); // 匿名委托,无引用!

// ✅ 正确:持有委托的引用直到 Native 不再使用
private readonly MyCallback _callback = OnEvent; // 类级别字段

void Init()
{
    var fnPtr = Marshal.GetFunctionPointerForDelegate(_callback);
    NativeMethods.RegisterCallback(fnPtr);
    GC.KeepAlive(_callback); // 明确告知 GC 此对象不可回收
}

⚠️ 跨平台注意

C/C++ 的 long 在 Windows 上是 32 位,在 macOS/Linux 上是 64 位。跨平台时应使用 .NET 6+ 提供的 CLong / CULong 类型,而不是 int 或 C# 的 long

内存管理:GC、Dispose 与非托管资源

"C# 有 GC,不用管内存" 是一个危险的误解。非托管资源(文件句柄、数据库连接、网络套接字)GC 不会自动释放,这是绝大多数内存泄漏的根源。

标准 Dispose 模式

持有非托管资源的类必须实现 IDisposable。以下是经典实现模式:

public class ResourceHolder : IDisposable
{
    private IntPtr _nativeHandle;     // 非托管资源
    private Stream _managedStream;    // 托管的 IDisposable
    private bool _disposed = false;

    // 公共方法:供调用方手动释放
    public void Dispose()
    {
        Dispose(disposing: true);
        GC.SuppressFinalize(this); // 告知 GC 不必再调用析构函数
    }

    // 核心释放逻辑
    protected virtual void Dispose(bool disposing)
    {
        if (_disposed) return;

        if (disposing)
        {
            // 释放托管资源(只在主动 Dispose 时)
            _managedStream?.Dispose();
        }

        // 释放非托管资源(无论哪种路径都要释放)
        if (_nativeHandle != IntPtr.Zero)
        {
            NativeFree(_nativeHandle);
            _nativeHandle = IntPtr.Zero;
        }

        _disposed = true;
    }

    // 析构函数:GC 兜底(不能保证调用时机)
    ~ResourceHolder() => Dispose(disposing: false);
}

异步 Dispose:IAsyncDisposable

.NET Core 3.0+ 引入了 IAsyncDisposable,用于需要异步释放资源的场景(如关闭网络连接需要发送 FIN 包)。配合 await using 语法使用:

public class AsyncConnection : IAsyncDisposable
{
    private readonly NetworkStream _stream;

    public async ValueTask DisposeAsync()
    {
        await _stream.FlushAsync();  // 异步刷新缓冲区
        await _stream.DisposeAsync(); // 异步关闭连接
    }
}

// await using 确保无论是否异常都会调用 DisposeAsync
await using var conn = new AsyncConnection(endpoint);
await conn.SendAsync(data);

💡 性能提示

返回 ValueTask 而不是 Task 可以在同步完成的情况下避免堆分配。当你的异步方法大多数时候能同步完成(如缓存命中)时,ValueTask 能显著提升性能。

写出健壮异步代码的自查清单

在提交 PR 之前,不妨过一遍这份清单:

  • TAP 方法名以 Async 结尾,返回 TaskTask<T>
  • 参数验证在 async 方法外层同步完成,不被包裹进 Task
  • 接受 CancellationToken 的方法在循环或 I/O 前检查取消状态
  • 返回 Task 的方法不在同步路径上长时间阻塞
  • 没有 .Result.Wait()(ASP.NET 环境中极易死锁)
  • 持有非托管资源的类实现了 IDisposable,并在 Dispose(false) 中释放非托管部分
  • 向 Native 代码传递的委托有足够长的生命周期(类字段或 GC.KeepAlive
  • 使用 BlockingCollection 时生产者最终调用了 CompleteAdding()
  • 并发访问 ConcurrentDictionary 的复合操作用了适当的原子方法或锁
  • async void 仅用于事件处理器,其他任何地方都应返回 Task

📚 深入阅读

本文内容均来自 Microsoft 官方 .NET 高级编程文档。建议系统阅读 TAP 实现模式任务并行库P/Invoke 最佳实践三个章节,收益最大。