【Hi.Ltd 专题】第7期:Threading 采集线程、锁与 LRU 缓存

Hi.Ltd 专题第 7 期 |模块:Threading |NuGet:Hi.Ltd 2026.7.11.1437

类型:Hi.Ltd.Threading.LRUCache<TKey,TValue> / HybridLocked / PropertyUtils / PropertyMethod<T> / DisposeHelper<T>;另 Hi.Ltd.OneManyLock / OneManyLockStates(根命名空间)。

场景:多设备采集最近值缓存、读写锁、短临界区、插件属性高速读写。

第6期:IO 文件导出、磁盘空间与落盘


本期目标

  1. LRUCache 做站号/标签最近值缓存,控制内存上限(默认容量 1024)
  2. HybridLocked(自旋+内核混合)做短临界区
  3. OneManyLock.Enter(exclusive) 做一写多读
  4. PropertyUtils / PropertyMethod<T> 降低反射开销(插件/绑定)
  5. 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> 三套)、AddOrUpdateTryGet/TryGetValueTryAddTryUpdateTryRemove、索引器
  • 查询:ContainsKeyCountIsEmptyKeys/Values(最近→最久)、GetKeys/GetValuesToArray
  • 生命周期:ClearDisposeThrowIfDisposed
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 状态字符串

状态枚举 OneManyLockStatesFree / 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#》

可能出现的问题(现场坑)

  1. LRU 过小 → PLC 读放大;过大 → 内存涨。
  2. 锁内 IO → 卡死采集周期。
  3. Invoked:README 有、DLL 无。
  4. 索引器取不存在键KeyNotFoundException;优先 TryGetValue
  5. 结构体 SetPropertyValue 必须接回返回值。
  6. OneManyLock 忘记 Leave → 写者饿死读者。

异常点与应对

现象 应对
内存涨 降 capacity;勿缓存大数组副本
读到旧值 写后 AddOrUpdate;快照用读写锁
锁卡死 缩短临界区;查重入/忘记 Leave
ObjectDisposedException ThrowIfDisposed;生命周期与窗体对齐
Property 名错 ArgumentException------启动时校验绑定表

常见问题

Q: 和 Utilities SemaphoreResult

A: Utilities 是 Result+信号量辅助;本模块是专用缓存/锁类型。

Q: 默认容量?

A: XML:无参构造默认 1024


下篇预告

第8期:Managements 插件、单例与服务定位)


相关推荐
A_nanda1 小时前
C# 界面卡顿:从定位到根治
c#
人效思维笔记1 小时前
阿米巴推行前,经营者要先做好哪些准备?
经验分享·笔记·阿米巴·阿米巴经营·中国式阿米巴
2501_930707782 小时前
使用C#代码为新创建的 Word 文档创建目录
c#·word
hongyangcao3 小时前
12.8万科技岗位被“重组“,预算流向了算力
人工智能·经验分享·ai编程
czhc11400756634 小时前
同一个数,两把尺子:六组“看着一样、其实不一样“
c#
花北城4 小时前
【C#底层库】access_token授权鉴权验证
c#·鉴权·token·授权
rick9775 小时前
C# 动态代理与 DispatchProxy:从原理到实战的完整指南
c#
总线通信小百科5 小时前
汽车也能像手机一样远程升级?车载OTA原理解析
经验分享
tang_04275 小时前
【Hi.Ltd 专题】第8期:Managements 插件、单例与服务定位
经验分享·c#·hi.ltd系列