【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> 三套)、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#》

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

  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 插件、单例与服务定位)


相关推荐
光视界1 小时前
具身数采专项对比:ZED 双目相机 VS Intel RealSense,机器人数据集采集谁更合适?
经验分享·机器人
总线通信小百科11 小时前
车载以太网模块、交换机与媒体转换器:三类设备怎么选?
经验分享·车载以太网
2601_9672127214 小时前
安全型轨道式插座合规资质核查与工程选型技术要点
大数据·经验分享·安全
一只代码猿17 小时前
Screen Studio 免费邪修用法:剪视频完全无痛(附提示词)
经验分享
蒋胜山17 小时前
VBScript007-Excel的99乘法表
经验分享·batch
德思特17 小时前
实操指南|从“能驻网“到“能上网“:手机入网与公网业务实验室测试分享
经验分享·无线通信
览讯冷却19 小时前
冷却塔噪声的健康危害与法规要求的技术梳理
经验分享·笔记·科技·其他·百度·制造·业界资讯
学页技术极客19 小时前
「Skill市场」会重演「App Store」的故事吗
经验分享
恒锐丰科技林技术员20 小时前
EG2304L 高压半桥驱动芯片:中小功率电力电子方案优选器件
经验分享·嵌入式硬件·硬件工程
哎写bug的程序员21 小时前
关于github
经验分享