2026 年 C# / .NET 高性能编程的最佳实践
以下是 2026 年 C# / .NET 高性能编程的最佳实践总结(基于 .NET 8 / .NET 9 / .NET 10 的最新特性)。
性能优化的核心原则永远是:先测量,再优化(Measure First)。不要盲目优化,否则容易引入 Bug 或牺牲可维护性。
1. 性能优化的黄金法则(最重要)
- 使用 BenchmarkDotNet 做微基准测试(不要用 Stopwatch 手动测热路径)。
- 使用 Visual Studio Performance Profiler / dotnet-trace / PerfView 分析真实瓶颈(CPU、内存分配、GC)。
- 优先优化“热路径”(Hot Path):被调用非常频繁的代码(循环、解析、序列化等)。
- 升级到最新 .NET 版本(.NET 9/10):动态 PGO 默认开启,JIT 循环优化、向量化和 GC 都有显著提升,通常零代码改动即可获得 10-20% 性能收益。
2. 减少内存分配(Allocation)—— 性能优化的核心
内存分配和 GC 是 C# 性能杀手,尤其在高吞吐场景。
| 优化手段 | 推荐场景 | 预期收益 |
|---|---|---|
| Span / ReadOnlySpan | 字符串解析、缓冲区处理、切片 | 大幅减少分配和拷贝 |
| stackalloc + Span | 小型临时缓冲区(< 几 KB) | 零堆分配 |
| ArrayPool.Shared | 中大型数组重复使用 | 重用数组,减少 GC |
| struct 而非 class | 频繁创建的小对象 | 避免堆分配 |
| 避免 Boxing | 值类型放入 object / 非泛型集合 | 消除隐式装箱 |
示例(结合你之前的字典超时代码):
// 推荐:使用 TryGetValue + Span/结构体减少分配
if (m_VFCycleStartTimes.TryGetValue(ws.Id, out DateTime cycleStartTime))
{
double elapsed = (DateTime.Now - cycleStartTime).TotalSeconds;
// ...
}
3. 集合与字典相关优化(针对你的代码场景)
- ConcurrentDictionary:适合多线程场景,但
TryAdd/[] =操作仍有开销。热点路径下考虑自定义锁或 lock-free 结构。 - 避免频繁 TryRemove / TryAdd:如果可能,复用对象而不是反复增删。
- 使用值类型 Key(struct)时注意避免装箱。
- 容量预分配:
new Dictionary<TKey, TValue>(capacity)或ConcurrentDictionary构造函数传入初始容量,减少扩容分配。
4. 异步与 I/O 优化
- 始终使用 async/await 处理 I/O(数据库、文件、网络)。避免
Task.Run滥用。 - 不要在热路径上做同步阻塞(
.Result、.Wait())。 - 数据访问:使用异步 API,只读取必要字段,避免 N+1 查询,合理使用缓存(MemoryCache / HybridCache / Redis)。
5. 循环与算法优化
- 减少循环内的分配和计算。
- 利用 JIT 的 SIMD 向量化(.NET 9+ 对循环优化很强)。
- 优先使用
for而不是foreach(在某些 Span/数组场景下)。 - 避免在循环中调用 LINQ(除非是编译后的 Compiled LINQ)。
6. 现代 C# / .NET 高性能特性(2025-2026 强烈推荐)
- Dynamic PGO(.NET 8+ 默认开启):让运行时根据实际执行模式优化代码。
- Native AOT:启动快、内存占用低,适合微服务、云原生场景。
- Source Generators:代替反射,减少运行时开销。
- System.Threading.Lock(C# 13+):比传统
lock(object)更安全高效。 - 隐式 Span 转换 等新特性:减少手动内存管理。
7. 其他实用最佳实践
- 缓存激进:频繁读取的数据一定要缓存。
- JSON 序列化:使用
System.Text.Json+ Source Generator,避免 Newtonsoft.Json。 - 字符串处理:优先
Span<char>+string.Create,少用+或String.Format。 - 异常处理:不要在热路径上依赖异常控制流(异常成本较高)。
- 发布时配置:Release 模式 + ReadyToRun + Tiered Compilation + PGO。
给你的建议(结合之前代码)
你当前的代码主要关注 超时检测 + 内存清理,属于防止内存泄漏/增长的防御性优化。进一步可以:
- 把
CheckAndHandleVFCycleTimeout封装成独立方法,便于 Benchmark。 - 如果
m_VFCycleStartTimes是高频访问的 ConcurrentDictionary,考虑是否能用更轻量的结构(如固定大小数组 + 锁)。 - 在
ClearAllVfCaches中大量使用ArrayPool归还数组,并尽量避免Clear()(如果数组内容不敏感)。
一句话总结:
先用 BenchmarkDotNet 和 Profiler 找到真正的热点 → 重点减少分配(Span + ArrayPool + struct)→ 升级 .NET 版本享受免费优化 → 最后再考虑算法/架构级优化。
好的,我帮你把代码修改成:如果 ws.Id 已存在,就更新时间;如果不存在,就添加。
修改后的代码:
m_VFCycleStartTimes[ws.Id] = DateTime.Now;
解释:
TryAdd(ws.Id, DateTime.Now):只有当 key 不存在时才添加,存在则不做任何操作。m_VFCycleStartTimes[ws.Id] = DateTime.Now;:这是最简洁的写法,存在就更新,不存在就添加(相当于AddOrUpdate的效果)。
如果你想写得更明确(推荐在重要代码中使用),可以这样写:
if (m_VFCycleStartTimes.ContainsKey(ws.Id))
{
m_VFCycleStartTimes[ws.Id] = DateTime.Now; // 更新
}
else
{
m_VFCycleStartTimes.TryAdd(ws.Id, DateTime.Now); // 添加
}
或者使用 AddOrUpdate(需要引入 System.Collections.Concurrent 的扩展):
m_VFCycleStartTimes.AddOrUpdate(ws.Id, DateTime.Now, (key, oldValue) => DateTime.Now);
最推荐的写法(简洁且高效):
m_VFCycleStartTimes[ws.Id] = DateTime.Now;
ConcurrentDictionary<TKey, TValue> 是 .NET 中专门为多线程环境设计的线程安全字典,位于 System.Collections.Concurrent 命名空间。
1. 什么是线程安全?
ConcurrentDictionary 的线程安全含义是:
- 所有公开的公共方法和受保护成员都可以被多个线程同时调用,而不会导致数据损坏、异常或不一致的状态。
- 你不需要在外部手动加
lock来保护字典本身的操作。 - 它能安全地处理并发读 + 并发写的场景。
官方文档明确说明:
“All public and protected members of ConcurrentDictionary<TKey,TValue> are thread-safe and may be used concurrently from multiple threads.”
2. 内部实现原理(Fine-Grained Locking + Lock-Free Read)
ConcurrentDictionary 并不是完全“无锁”(Lock-Free),而是采用**细粒度锁(Fine-Grained Locking)**策略:
- 内部结构:它把哈希表分成多个 桶(buckets),每个桶(或一组桶)有一个独立的锁。
- 写入操作(Add、TryAdd、TryRemove、[] = 等):只锁定当前 key 所在的桶,而不是锁整个字典。因此多个线程操作不同 key 时可以真正并行执行,减少锁争用(contention)。
- 读取操作(TryGetValue、ContainsKey、this[key] 等):完全无锁(Lock-Free),性能非常高。即使有线程在写入,读操作也能安全进行。
- 枚举(foreach、Count):会锁定所有桶,相对较重,不建议在高并发热路径中使用。
这种设计让它在高读写并发场景下比“普通 Dictionary + 一把大锁”有更好的伸缩性(Scalability)。
3. 重要注意事项(不是完全原子)
虽然单个方法是线程安全的,但复合操作不一定是原子的:
GetOrAdd和AddOrUpdate是最容易出错的地方:- 它们接受的委托(valueFactory 或 updateFactory)在锁外面执行。
- 原因:防止用户代码长时间阻塞导致所有线程都被卡住。
- 后果:多个线程同时调用
GetOrAdd时,工厂方法可能会被执行多次(只有第一个成功插入的值会被保留)。
示例风险:
// 可能多次执行 expensiveFactory
var value = dict.GetOrAdd(key, k => expensiveFactory(k));
推荐安全写法(如果你希望工厂只执行一次):
var lazyValue = dict.GetOrAdd(key, k => new Lazy<ValueType>(() => expensiveFactory(k)));
var finalValue = lazyValue.Value; // Lazy 会保证只初始化一次
4. 常用线程安全方法总结
| 方法 | 线程安全 | 是否原子 | 备注 |
|---|---|---|---|
| TryAdd | 是 | 是 | 推荐添加方式 |
| TryGetValue | 是 | 是 | 推荐读取方式 |
| TryRemove | 是 | 是 | 推荐移除方式 |
| [] = (索引器赋值) | 是 | 是 | 存在则更新,不存在则添加 |
| GetOrAdd | 是 | 部分 | 工厂可能多次执行 |
| AddOrUpdate | 是 | 部分 | 同上 |
| Count / foreach | 是 | 是 | 会锁整个字典,较重 |
5. 与普通 Dictionary + lock 的对比
- Dictionary + lock:简单粗暴,一把大锁保护所有操作。在低并发时性能可能更好;在高并发时锁争用严重,吞吐量差。
- ConcurrentDictionary:细粒度锁 + 读无锁,高并发下伸缩性更好,但单个操作有一定 overhead(尤其在字典很大、频繁增删时)。
- 性能建议:
- 低并发或字典很大(>10万项)且写操作极少 → 考虑
Dictionary + lock可能更快。 - 高并发(多线程频繁读写)→ 强烈推荐 ConcurrentDictionary。
- 低并发或字典很大(>10万项)且写操作极少 → 考虑
6. 回到你之前的代码
你之前使用:
m_VFCycleStartTimes.TryAdd(ws.Id, DateTime.Now); // 存在则不添加
m_VFCycleStartTimes[ws.Id] = DateTime.Now; // 存在则更新,不存在则添加
m_VFCycleStartTimes.TryGetValue(...);
m_VFCycleStartTimes.TryRemove(...);
这些操作都是线程安全的,你可以放心在多线程环境中直接调用,不需要额外加锁。
一句话总结:
ConcurrentDictionary 通过细粒度锁实现了“多个线程可以安全地同时读写字典”,读操作几乎无锁,写操作只锁很小的范围,从而在高并发场景下提供良好的性能和安全性。但 GetOrAdd/AddOrUpdate 中的委托不是在锁内执行,需要特别注意。
是的,我来帮你把之前的超时检测逻辑进一步优化,并加上带日志 + 其他实用逻辑。
请先告诉我你的项目具体情况,以便我给出最针对性的优化:
快速问题(回复时直接回答即可):
-
这是实时/工控类项目吗?
(例如:MES 系统、自动化产线、设备数据采集、视觉检测(VF = Vision Framework?)、高频 PLC/传感器数据处理等) -
m_VFCycleStartTimes这个字典的访问频率大概是怎样的?- 每秒几十次?几百次?还是上千次?
- 工位(ws.Id)数量通常是多少?(几十个?几百个?)
-
你希望增加哪些额外逻辑?(可多选)
- 超时分级报警(例如 >300s 警告,>500s 严重停止)
- 记录超时持续时间、历史最大超时时间
- 超时后自动重置工位状态并重新开始新周期
- 增加性能监控(本次检测耗时、字典大小等)
- 防止短时间内重复触发停止(防抖动)
- 其他(如邮件/报警推送、保存现场数据等)
目前推荐的优化版本(带日志 + 结构化处理)
以下是结合实时场景的改进版代码:
/// <summary>
/// 检查VF周期是否超时,并处理超时逻辑(推荐封装成独立方法)
/// </summary>
/// <returns>是否发生超时并已停止</returns>
private bool CheckAndHandleVFCycleTimeout(Workstation ws)
{
if (ws == null) return false;
// 1. 安全获取开始时间(高频路径,尽量减少分配)
if (!m_VFCycleStartTimes.TryGetValue(ws.Id, out DateTime cycleStartTime))
{
// 可选:记录一下异常情况
// LogAction.LogMsg($"[Warn] Workstation {ws.Name} has no cycle start time.", 1, 1);
return false;
}
double elapsedSeconds = (DateTime.Now - cycleStartTime).TotalSeconds;
// 2. 未达到阈值 → 正常返回
if (m_VFCoolStopFlag.TryGetValue(ws.Id, out bool isStopFlag) && isStopFlag)
{
return false; // 已标记停止,不再处理
}
if (elapsedSeconds <= 500)
{
return false;
}
// 3. 超时处理
LogAction.LogMsg($"VF cycle timeout detected → {ws.Name} (Id: {ws.Id}), " +
$"已运行 {elapsedSeconds:F1} 秒(阈值 500s),执行停止保护。", 2, 2);
try
{
// 彻底清理缓存,防止内存持续增长(这是你最关心的点)
ClearAllVfCaches(ws);
// 重置标记位
ResetVfFlags(ws);
// 移除计时器
m_VFCycleStartTimes.TryRemove(ws.Id, out _);
// 可选:记录本次超时信息(便于后续统计分析)
// m_TimeoutHistory.TryAdd(ws.Id, DateTime.Now);
// ====================== 执行停止 ======================
Stop(); // 请替换为你的实际停止逻辑
return true; // 已处理超时
}
catch (Exception ex)
{
LogAction.LogMsg($"处理VF超时逻辑时发生异常 {ws.Name}: {ex.Message}", 3, 2);
return false;
}
}
调用方式(建议在你的数据处理循环中调用):
// 在处理每个工位数据的热路径中
if (CheckAndHandleVFCycleTimeout(ws))
{
return; // 立即退出本次处理
}
// 正常业务逻辑...
下一步优化方向:
- 如果工位数量少且固定 → 考虑用
Dictionary + ReaderWriterLockSlim或数组 + 锁,可能比ConcurrentDictionary更快。 - 如果高并发、高频更新 → 保留
ConcurrentDictionary,但可调整构造函数的concurrencyLevel。 - 引入定时扫描线程(而非每次数据都检查),降低热路径开销。
- 使用
ValueTuple或自定义轻量结构体减少内存分配。 - 增加超时统计(平均超时、最大超时、超时次数等)。
更多推荐

所有评论(0)