机制图解|OpenTAP Resource Manager 资源生命周期一行行梳理

背景

在 OpenTAP 里,「资源」不只是万用表或示波器,任何需要「打开→使用→关闭」的实体都被抽象成 IResource。Resource Manager(下文简称 RM)就是幕后管家:它负责按需启停、冲突检测、引用计数,还要保证 TestPlan 无论正常结束还是异常取消,仪器都能安全下电。官方文档只告诉你“把仪器拖进测试计划就好”,却没人讲 RM 怎么知道何时该 Open()、何时该 Close()。今天把源码拆到一行行,把这个黑盒照亮。

框架分析

RM 的核心代码集中在 Engine/ResourceManager.cs,对外只暴露三个关键 API:

  1. Open(CancellationToken token) – 拓扑排序后按依赖顺序打开资源。
  2. Close() – 逆序关闭,引用计数归零才真正 Close()
  3. GetResource<T>() – 查询已缓存实例,支持按类型、名称、接口多重过滤。

底层用两张表:

  • Dictionary<IResourceNode, ResourceWrapper> _wrappers – 节点 → 包装器,包装器里存引用计数、实例对象、打开状态。
  • List<IResourceNode> _openedInOrder – 按打开先后记录,保证关闭时逆序。

实现过程

1. 拓扑打开:防止「A 依赖 B,却先开 A」的尴尬

1
2
3
4
5
6
7
8
9
10
11
12
13
// ResourceManager.Open 节选
var sorted = TopologicalSort(_plan.ResourceNodes);
foreach (var node in sorted)
{
var wrapper = _wrappers[node];
if (wrapper.RefCount == 0) // 第一次用到才实例化
{
wrapper.Instance = node.CreateResource();
wrapper.Instance.Open(token); // 可能抛异常,外层会触发回滚
_openedInOrder.Add(node);
}
wrapper.RefCount++;
}

TopologicalSort 用 DFS 实现,时间复杂度 O(V+E),保证依赖资源先就绪。

2. 引用计数:同一路由被多个 Step 复用也不重复开关

1
2
3
4
5
6
7
8
// ResourceManager.GetResource<T> 节选
public T GetResource<T>(string name) where T : class, IResource
{
var wrapper = _wrappers.Values.FirstOrDefault(w => w.Instance is T t && t.Name == name);
if (wrapper == null) throw new ResourceNotFoundException(name);
wrapper.RefCount++; // 关键:只是计数,不重复 Open
return (T)wrapper.Instance;
}

3. 逆序关闭:异常场景也能安全下电

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ResourceManager.Close 节选
for (int i = _openedInOrder.Count - 1; i >= 0; i--)
{
var node = _openedInOrder[i];
var wrapper = _wrappers[node];
if (--wrapper.RefCount == 0)
{
try
{
wrapper.Instance.Close();
}
catch (Exception ex)
{
Log.Error($"Close {wrapper.Instance.Name} failed: {ex.Message}");
// 继续关闭其余资源,不能因一台仪器抛异常就漏关下一台
}
wrapper.Instance = null;
}
}

注意事项

  1. 自己写 Step 时,不要把 IResource 存成字段后就不管,一旦 TestPlan 被提前取消,RM 只会调 Close(),不会帮你 Dispose();需要一次性清理请实现 IDisposable 并在 Close() 里调用。
  2. 若资源构造函数里就抛异常,RM 会立即触发 Close() 已打开的部分,但不会回滚构造函数副作用——如果你在构造函数里把仪器状态改了,记得自己捕获并还原。
  3. 命名重复不会编译期报错,RM 在运行期按「先匹配类型→再匹配名称」策略,可能拿到意料之外的实例;保证仪器别名全局唯一最省心。

可复现命令

以下最小示例演示 RM 的引用计数行为:

1
2
3
4
5
6
7
# 克隆示例(已含最小插件)
git clone https://github.com/yourname/opentap-lab.git
cd opentap-lab/ResourceLifecycle
# 安装依赖
tap package install -y
# 运行计划(两个 Step 共用同一台 DC Power)
tap run testplan/ShareSameResource.TapPlan

日志里能看到:

  • 第一条 Step 打开电源,RefCount=1
  • 第二条 Step 复用,RefCount=2
  • 计划结束一次性 Close,RefCount 归零才真正下电

小结

Resource Manager 的源码不到 600 行,却把「依赖排序」「引用计数」「异常安全」三件事做得干净利落:拓扑保证顺序,计数避免重复,逆序+try/catch 保证异常也不漏关。下次再拖仪器到测试计划,你知道背后有人帮你数着引用、排着队、守着最后一盏灯熄灭。

关键源码路径

  • Engine/ResourceManager.cs – 核心实现
  • Engine/IResource.cs – 资源接口定义
  • Engine/IResourceNode.cs – 计划节点与资源绑定
  • Engine/ResourceSettings.cs – 资源别名、序列化配置