前言
「幫幫我,我負載過重」——這是許多 C# 伺服器開發者的心聲。本文從語言層面的運算子多載設計原則切入,延伸至非同步程式設計、記憶體/GC 優化、鎖競爭降低等實戰技巧,協助開發者診斷並解決高負載瓶頸。
運算子多載:何時使用、何時避免
適合多載的場景 (語意直覺、符合數學/領域慣例)
| 型別 | 運算子 | 語意 | 範例 |
| 巨量整數 | + - * / | 算術運算 | BigInt a = 123...; BigInt b = a + c; |
| 向量/矩陣 | + - * (純量/點積/叉積) | 線性代數 | Vector v = v1 + v2; Matrix m = m1 * m2; |
| 複數 | + - * / | 複數運算 | Complex z = z1 * z2; |
| 日期時間區間 | + - | 時間位移 | DateTime end = start + duration; |
| 貨幣/度量 | + - * / | 單位一致運算 | Money total = price * quantity; |
不適合多載的場景 (語意模糊、違反最小驚訝原則)
- 資料庫游標遞增:
row++ → 改用 row.MoveNext()。
- 帳戶餘額異動:
account += 5 → 改用 account.Deposit(5) 或 account.Withdraw(5)。
- 字串非直覺運算:
str1 * str2 → 無明確領域含義。
實作規範
public struct Vector2
{
public double X, Y;
public Vector2(double x, double y) { X = x; Y = y; }
// 必須 static、至少一參數為當前型別、回傳新實例 (不可變)
public static Vector2 operator +(Vector2 a, Vector2 b)
=> new Vector2(a.X + b.X, a.Y + b.Y);
public static Vector2 operator -(Vector2 a, Vector2 b)
=> new Vector2(a.X - b.X, a.Y - b.Y);
public static Vector2 operator *(Vector2 v, double scalar)
=> new Vector2(v.X * scalar, v.Y * scalar);
public static Vector2 operator *(double scalar, Vector2 v) => v * scalar; // 交換律
public double Length => Math.Sqrt(X * X + Y * Y); // 唯讀屬性優於運算子
// 點積:不多載 * (語意歧義),改用靜態方法
public static double Dot(Vector2 a, Vector2 b) => a.X * b.X + a.Y * b.Y;
}
隱式/顯式轉換運算子
- 隱式:資訊不遺失、永不拋例外 →
implicit operator Target(Source s)。
- 顯式:可能遺失精度/溢位/無效 →
explicit operator Target(Source s) 需強制轉型。
public struct Angle
{
public double Radians;
public static implicit operator Angle(double rad) => new Angle { Radians = rad };
public static explicit operator double(Angle a) => a.Radians; // 需 (double)angle
}
非同步程式設計:解決執行緒池耗盡
同步阻塞的代價
// 壞例:每個請求佔用一執行緒等待 I/O
public string GetDataSync()
{
using var client = new HttpClient();
return client.GetStringAsync("https://api.example.com").Result; // 死鎖風險 + 阻塞執行緒
}
正確的非同步模式
// 好例:全程 async/await,釋放執行緒回池
public async Task GetDataAsync(CancellationToken ct = default)
{
using var client = new HttpClient();
return await client.GetStringAsync("https://api.example.com", ct);
}
// Controller
[HttpGet]
public async Task Get(CancellationToken ct)
=> Ok(await _service.GetDataAsync(ct));
並行控制
SemaphoreSlim 限制並發 I/O 數量。
Task.WhenAll 批次等待、Task.WhenAny 超時控制。
- 避免
Task.Run 包裝同步 I/O (偽非同步),改用真正的非同步 API。
記憶體與 GC 優化
大物件堆積 (LOH) 壓力降低
- 大型陣列/緩衝區使用
ArrayPool<T>.Shared.Rent/Return 重複利用。
Memory<T>/Span<T> 避免副本切片。
- 大型字串建構用
StringBuilder 或 System.IO.Pipelines。
物件池模式
// 高頻分配物件 (如 SocketAsyncEventArgs、資料庫命令)
private readonly ObjectPool _cmdPool = new DefaultObjectPool(
new DbCommandPooledPolicy(), maximumRetained: 100);
public async Task ExecuteAsync(string sql)
{
DbCommand cmd = _cmdPool.Get();
try { /* 使用 cmd */ }
finally { _cmdPool.Return(cmd); }
}
結構體優於類別 (高頻小物件)
struct 配合 readonly 減少堆積配置、GC 掃描壓力。
- 避免裝箱:介面呼叫、
object 裝箱會配置堆積。
鎖競爭與同步化優化
| 問題 | 方案 |
| 粗粒度 lock | 縮小關鍵區、使用 ConcurrentDictionary/ConcurrentQueue 無鎖集合 |
| 讀多寫少 | ReaderWriterLockSlim 或 ImmutableDictionary (無鎖讀取) |
| 跨執行緒信號 | SemaphoreSlim, ManualResetEventSlim, Channels |
| 非同步等待鎖 | AsyncLock (Stephen Cleary) 或 SemaphoreSlim.WaitAsync |
診斷工具與指標
- dotnet-counters:即時監控 CPU、GC、ThreadPool、Lock 爭用。
- dotnet-trace / PerfView:ETW 追蹤、火焰圖、GC 統計。
- dotnet-dump:記憶體傾印分析 (SOS、ClrMD)。
- BenchmarkDotNet:微基準測試驗證優化成效。
關鍵指標閾值參考
| 指標 | 健康閾值 | 警示閾值 |
| Gen 2 GC 頻率 | < 1/分鐘 | > 10/分鐘 |
| LOH 佔用 | < 50 MB | > 200 MB |
| ThreadPool 佇列長度 | < 100 | > 1000 |
| 鎖等待時間 (avg) | < 1 ms | > 10 ms |
| 非同步完成佇列積壓 | < 50 | > 500 |
總結:優化優先順序
- 架構層面:非同步全程貫通、無狀態水平擴展、快取策略 (Redis/MemoryCache)。
- 程式碼層面:移除同步阻塞、物件池/ArrayPool、結構體替換高頻類別。
- 語言特性:合理運算子多載 (僅領域直覺)、隱式轉換謹慎、
ref struct/Span 零複製。
- 基礎設施:容器資源限制、GC 模式 (Server/Workstation)、.NET 版本升級 (長期支援版)。
「過早優化是萬惡之源」——先測量、再定位、再優化。使用 dotnet-counters 建立基準線,針對真正熱點施策,避免為微小增益犧牲可讀性。
- 上一篇文章: C# 程式碼撰寫規範:命名慣例、格式化、註釋與最佳實務完整指南
- 下一篇文章: 深入解析單例模式:從設計原理到 C# 實作與 .NET 應用
Copyright ©
程式碼之家 All Rights Reserved