.NET 进阶之路:异步、并发与内存管理的系统性认知
当前位置:点晴教程→知识管理交流
→『 技术文档交流 』
异步编程模式的演进与 TAP 最佳实践.NET 的异步编程经历了三个时代。理解这段历史不是为了考古,而是因为你在维护老代码时必然会遭遇它们,理解它们才能优雅地迁移。
TAP 方法的命名与签名规范很多人写异步方法时忽视规范,导致 API 设计混乱。TAP 有一套严格的约定:
Task 的生命周期:一个经常被忽视的细节
⚠️ 常见错误 如果你在 TAP 方法内部通过 异常处理的正确姿势异步方法中的异常处理有一个重要原则:参数验证异常应该在 async 方法外层同步抛出,这样调用者能立即捕获,而不必 await 后才能发现错误。
取消令牌与进度报告:让异步操作可控写了两三年 .NET,你可能已经在用 CancellationToken 的三种终态
取消时 Task 进入 最佳实践:在计算密集型任务中轮询取消
进度报告:IProgress
|
| 集合类型 | 适用场景 | 注意事项 |
|---|---|---|
ConcurrentDictionary |
多线程频繁读写键值对 | GetOrAdd / AddOrUpdate 非原子操作 |
ConcurrentQueue |
FIFO 生产者-消费者场景 | 枚举不保证顺序稳定 |
BlockingCollection |
有界缓冲 + 阻塞语义 | 需要配合 CompleteAdding() 正确关闭 |
ConcurrentBag |
混合生产者-消费者(同线程添加取出) | 纯生产消费场景比其他集合慢 |
这是很多人犯错的地方: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; // 真正的构造只发生一次
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 是调用 Windows API 或 C 库的标准方式,但很多 .NET 开发者很少接触。理解它的基本原理能帮你在需要时快速上手,也能读懂底层库的代码。
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);
}
将委托转换为函数指针传给 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。
"C# 有 GC,不用管内存" 是一个危险的误解。非托管资源(文件句柄、数据库连接、网络套接字)GC 不会自动释放,这是绝大多数内存泄漏的根源。
持有非托管资源的类必须实现 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);
}
.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 之前,不妨过一遍这份清单:
Async 结尾,返回 Task 或 Task<T>CancellationToken 的方法在循环或 I/O 前检查取消状态Task 的方法不在同步路径上长时间阻塞.Result 或 .Wait()(ASP.NET 环境中极易死锁)IDisposable,并在 Dispose(false) 中释放非托管部分GC.KeepAlive)BlockingCollection 时生产者最终调用了 CompleteAdding()ConcurrentDictionary 的复合操作用了适当的原子方法或锁async void 仅用于事件处理器,其他任何地方都应返回 Task📚 深入阅读
本文内容均来自 Microsoft 官方 .NET 高级编程文档。建议系统阅读 TAP 实现模式、任务并行库、P/Invoke 最佳实践三个章节,收益最大。
转自https://www.cnblogs.com/denglei1024/p/19803852