历史流式读 (ReadHistoryStreamAsync)
概述
IDE 趋势图 / HMI 历史曲线拉历史时, 一次性把万点装进 List<DataValue> 会让 UI 线程卡顿 + OOM。ReadHistoryStreamAsync 返回 IAsyncEnumerable<DataValue>, 业务侧用 await foreach 拿一条处理一条, 边拉边渲染。
对应规范段: Part 11 §6.5 (HistoryRead)。
API
| 方法 | 类别 | 读写 | 说明 |
|---|---|---|---|
| ua.ReadHistoryStreamAsync(nodeId, t0, t1, batchSize, ct) | 历史 | 读 | 异步流式读取 raw 历史 |
参数:
| 参数 | 类型 | 默认 | 说明 |
|---|---|---|---|
| nodeId | string | — | 历史节点 NodeId |
| startUtc | DateTime | — | 起始 UTC |
| endUtc | DateTime | — | 结束 UTC |
| batchSize | uint | 1000 | 每次 RPC 最多拿多少点 |
| cancellationToken | CancellationToken | default | 取消令牌 |
返回: IAsyncEnumerable<DataValue>, 每个元素 必须 Dispose (实现 IDisposable)。
代码示例
using DarraOpcUa_Client;
using var ua = new DarraOpcUa("opc.tcp://localhost:4840");
ua.Connect();
var t0 = DateTime.UtcNow.AddHours(-24);
var t1 = DateTime.UtcNow;
// 1) 边拉边渲染
await foreach (var dv in ua.ReadHistoryStreamAsync("ns=2;s=Temp", t0, t1))
{
using (dv)
{
chartSeries.Points.Add(dv.SourceTimestamp ?? DateTime.UtcNow, dv.Value.AsDouble);
}
}
// 2) 取消支持
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
try
{
await foreach (var dv in ua.ReadHistoryStreamAsync("ns=2;s=Temp", t0, t1, ct: cts.Token))
using (dv) Process(dv);
}
catch (OperationCanceledException)
{
Console.WriteLine("超时, 已部分加载");
}
// 3) LINQ 风格筛选 (System.Linq.Async)
var bad = await ua.ReadHistoryStreamAsync("ns=2;s=Temp", t0, t1)
.Where(dv => dv.Status != StatusCode.Good)
.CountAsync();
Console.WriteLine($"24h 内有 {bad} 个坏值");
实现限制
- 当前底层
ReadHistory不暴露ContinuationPoint, 一次 RPC 返回区间内所有点 (受batchSize限制) - 大区间业务侧需自行切片 — 例如把 24 小时拆成 24 段 1 小时, 串行
await foreach - 取消立即返回当前已拿部分, 不抛
OperationCanceledException(除非 cts 触发在 RPC 期间)
与 ReadHistory 的对比
// 同步: 一次拿万点, UI 卡 1 秒
var vals = ua.ReadHistory("ns=2;s=Temp", t0, t1, maxValues: 50000);
foreach (var dv in vals)
using (dv) chartSeries.Points.Add(...);
// 异步流式: 边拉边渲染, UI 始终响应
await foreach (var dv in ua.ReadHistoryStreamAsync("ns=2;s=Temp", t0, t1))
using (dv) chartSeries.Points.Add(...);
最佳实践
- 必须
using每条 dv — 漏 Dispose 会泄漏 native 内存 - 取消令牌 — 长查询给用户"取消"按钮
- 分段策略 — 区间 > 1 小时建议外层手动切片, 防服务端单次返回上限触发截断
- UI 渲染节流 — 每 100 条调用一次
chart.Refresh(), 不要每条都刷
跨语言对照
| C# | Python | Java | C++ | Rust | C |
|---|---|---|---|---|---|
| ReadHistoryStreamAsync | read_history_stream | readHistoryStream | ReadHistoryStreamAsync | read_history_stream | DarraUa_Session_ReadHistoryStream |