.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