Hi.Ltd 专题第 7 期 |模块:Threading |NuGet:
Hi.Ltd2026.7.11.1437类型:
Hi.Ltd.Threading.LRUCache<TKey,TValue>/HybridLocked/PropertyUtils/PropertyMethod<T>/DisposeHelper<T>;另Hi.Ltd.OneManyLock/OneManyLockStates(根命名空间)。场景:多设备采集最近值缓存、读写锁、短临界区、插件属性高速读写。
本期目标
- 用
LRUCache做站号/标签最近值缓存,控制内存上限(默认容量 1024) - 用
HybridLocked(自旋+内核混合)做短临界区 - 用
OneManyLock.Enter(exclusive)做一写多读 - 用
PropertyUtils/PropertyMethod<T>降低反射开销(插件/绑定) - 用
DisposeHelper<T>.Dispose安全置空释放
适用范围
| 场景 | 说明 |
|---|---|
| 多 PLC 标签最近一次读值 | LRUCache<string, short>,容量按站点数估算 |
| UI 读快照、采集线程写快照 | OneManyLock:读 Enter(false),写 Enter(true) |
| 更新内存字典几行代码 | HybridLocked 短临界区 |
| 插件属性注入、配方对象动态赋值 | PropertyUtils / PropertyMethod 表达式树编译 |
不该用 / 慎用
| 场景 | 说明 |
|---|---|
| 把 LRU 当历史曲线库 | 只保留「最近」;历史进数据库/文件 |
| 锁内做网络/PLC IO | 易死锁与卡顿------锁内只改内存 |
README Invoked |
DLL 无此类型 |
忘记 Dispose 缓存/锁 |
资源与句柄泄漏 |
LRUCache<TKey,TValue>
LRU(Least Recently Used,最近最少使用)缓存是一种常见的缓存淘汰策略:当缓存容量满时,优先淘汰最久未被访问的数据。当前为线程安全实现,构造:
| 构造 | 说明 |
|---|---|
new() |
默认最大容量 1024 |
new(capacity) |
指定容量;满则淘汰最久未用 |
new(capacity, concurrencyLevel) |
并发级别 |
new(comparer) / 三参 |
自定义键比较 |
常用成员(DLL):
- 读改:
GetOrAdd(值 /Func/Func<TKey,TValue>三套)、AddOrUpdate、TryGet/TryGetValue、TryAdd、TryUpdate、TryRemove、索引器 - 查询:
ContainsKey、Count、IsEmpty、Keys/Values(最近→最久)、GetKeys/GetValues、ToArray - 生命周期:
Clear、Dispose、ThrowIfDisposed
csharp
using Hi.Ltd.Threading;
var cache = new LRUCache<string, short>(capacity: 2048);
// 未命中时读 PLC(示意)
short d100 = cache.GetOrAdd("PLC1.D100", key => ReadHolding(key));
cache.AddOrUpdate("PLC1.D100", (short)1234, (k, old) => (short)1234);
if (cache.TryGetValue("PLC1.D100", out short v))
{
// 显示到 HMI
}
cache.Dispose();
工控提示:capacity 按「站点数 × 每站标签数」的热集合估算;过小会放大 PLC 读,过大吃内存。
HybridLocked
混合锁:结合自旋与内核等待,适合短临界区。
| 成员 | 说明 |
|---|---|
Enter / Exit |
进入/退出 |
IsBusy |
是否占用 |
Dispose |
释放内核事件等 |
csharp
var gate = new HybridLocked();
gate.Enter();
try
{
// 仅更新内存快照
_snapshot = newSnapshot;
}
finally { gate.Exit(); }
OneManyLock(Hi.Ltd.OneManyLock)
多读者 / 单写者。
| API | 说明 |
|---|---|
Enter(bool exclusive) |
true=写独占;false=读共享 |
Leave |
释放 |
Dispose |
释放 |
ToString |
状态字符串 |
状态枚举 OneManyLockStates:Free / OwnedByReaders / OwnedByReadersAndWriterPending / OwnedByWriter / ReservedForWriter。
csharp
using Hi.Ltd;
var rw = new OneManyLock();
// 采集写
rw.Enter(exclusive: true);
try { _tags["D100"] = value; }
finally { rw.Leave(); }
// UI 读
rw.Enter(exclusive: false);
try { return _tags["D100"]; }
finally { rw.Leave(); }
PropertyUtils / PropertyMethod<T>
表达式树编译 getter/setter,避免热路径裸反射。
csharp
using Hi.Ltd.Threading;
using System.Reflection;
var axis = new AxisDto { Speed = 100 };
object spd = PropertyUtils.GetPropertyValue(axis, "Speed");
axis = PropertyUtils.SetPropertyValue(axis, "Speed", 120); // 结构体返回新实例
var pi = typeof(AxisDto).GetProperty("Speed");
var getter = PropertyMethod<AxisDto>.GetGetter(pi);
var setter = PropertyMethod<AxisDto>.GetSetter(pi); // 仅 class;struct 用 GetStructSetter
DisposeHelper<T>.Dispose(ref item):执行释放并将引用置空。
完整示例:多 PLC 采样缓存 + 读写锁快照
csharp
using System;
using System.Collections.Generic;
using Hi.Ltd;
using Hi.Ltd.Threading;
public sealed class TagHub : IDisposable
{
private readonly LRUCache<string, object> _cache = new(4096);
private readonly OneManyLock _rw = new();
private readonly HybridLocked _publish = new();
private Dictionary<string, object> _uiSnapshot = new();
public T GetOrRead<T>(string tag, Func<T> reader)
{
return (T)_cache.GetOrAdd(tag, _ => reader()!);
}
public void WriteTag(string tag, object value)
{
_cache.AddOrUpdate(tag, value, (_, __) => value);
_rw.Enter(true);
try { _uiSnapshot[tag] = value; }
finally { _rw.Leave(); }
}
public IReadOnlyDictionary<string, object> CaptureForUi()
{
_rw.Enter(false);
try { return new Dictionary<string, object>(_uiSnapshot); }
finally { _rw.Leave(); }
}
public void PublishBatch(Action update)
{
_publish.Enter();
try { update(); }
finally { _publish.Exit(); }
}
public void Dispose()
{
_cache.Dispose();
_rw.Dispose();
_publish.Dispose();
}
}
LRUCache 与采集周期协作(进阶)
csharp
using System;
using System.Threading;
using System.Threading.Tasks;
using Hi.Ltd.Threading;
public sealed class PollService : IDisposable
{
private readonly LRUCache<string, short> _cache = new(capacity: 4096, concurrencyLevel: 8);
private readonly HybridLocked _gate = new();
private CancellationTokenSource _cts;
public void Start()
{
_cts = new CancellationTokenSource();
_ = Task.Run(() => LoopAsync(_cts.Token));
}
private async Task LoopAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
// 示意:批量读后写入缓存;锁外做 IO
short d100 = ReadPlc("D100");
_cache.AddOrUpdate("D100", d100, (_, __) => d100);
_gate.Enter();
try { /* 更新极短 UI 标记 */ }
finally { _gate.Exit(); }
await Task.Delay(200, ct).ConfigureAwait(false);
}
}
public bool TryGet(string tag, out short value) => _cache.TryGetValue(tag, out value);
private static short ReadPlc(string tag) => 0;
public void Dispose()
{
_cts?.Cancel();
_cts?.Dispose();
_cache.Dispose();
_gate.Dispose();
}
}
要点 :PLC 读在锁外;AddOrUpdate 本身线程安全;HybridLocked 只保护非缓存的共享小状态。
OneManyLockStates 观测
csharp
using Hi.Ltd;
var rw = new OneManyLock();
rw.Enter(false);
try
{
// ToString 可辅助诊断;状态枚举见 OneManyLockStates
$"lock={rw}".Debug();
}
finally { rw.Leave(); }
状态机:Free → 读者 OwnedByReaders;写者等待时 OwnedByReadersAndWriterPending;独占 OwnedByWriter / ReservedForWriter。
注: 算法思路参考自《CLR via C#》
可能出现的问题(现场坑)
- LRU 过小 → PLC 读放大;过大 → 内存涨。
- 锁内 IO → 卡死采集周期。
- Invoked:README 有、DLL 无。
- 索引器取不存在键 →
KeyNotFoundException;优先TryGetValue。 - 结构体 SetPropertyValue 必须接回返回值。
- OneManyLock 忘记 Leave → 写者饿死读者。
异常点与应对
| 现象 | 应对 |
|---|---|
| 内存涨 | 降 capacity;勿缓存大数组副本 |
| 读到旧值 | 写后 AddOrUpdate;快照用读写锁 |
| 锁卡死 | 缩短临界区;查重入/忘记 Leave |
| ObjectDisposedException | ThrowIfDisposed;生命周期与窗体对齐 |
| Property 名错 | ArgumentException------启动时校验绑定表 |
常见问题
Q: 和 Utilities SemaphoreResult?
A: Utilities 是 Result+信号量辅助;本模块是专用缓存/锁类型。
Q: 默认容量?
A: XML:无参构造默认 1024。