C# 处理体数据:装、钉、交、取消

C# 处理体数据:装、钉、交、取消

今天聊的是一条线:工业检测软件里,C# 要把一大块「体数据」交给 C++ 算法去算。这件事拆成四个动作------ (用什么装)、 (为什么钉)、 (怎么交地址)、取消(怎么安全喊停)。下面一个个讲,概念用真名,代码脱敏示意。


一、体数据是什么:3D 的「像素」

体素(voxel) 就是 3D 版的「像素(pixel)」。

像素 pixel 体素 voxel
维度 2D 平面 3D 立体
样子 一个小方格 一个小立方块
组成 照片 = 宽 × 高 个像素 体数据 = 长 × 宽 × 高 个体素

生活类比:体素就是 Minecraft 里的小方块。Minecraft 世界由无数小方块堆成 3D 世界;体数据也是这样,每个小方块(体素)代表那一小块空间里的密度/灰度。

CT 重建干的事,就是把多角度的 2D 投影图,重建成一个由体素堆起来的 3D 体数据。


二、为什么用 byte[] 装:因为它是一大块「裸字节」

体数据本质是一大块连续的、无类型的原始字节。它不是「一堆整数」,也不是「一堆小数」,就是「一堆字节」。

所以用 byte[] 装,理由三条:

  1. byte 是内存最小单位(1 字节)byte[] 就是精确要「这么多字节的连续内存」,一个不多一个不少。
  2. 数组元素在内存里连续存放,首地址能直接交给 C++。
  3. C# 不贴标签------不解释这些字节是什么,C++ 自己按它的规则解释。

生活类比:体数据像一箱沙 。C++ 要的是「沙子从哪开始、一共多少粒」,每粒沙没有类型标签。byte 就是一粒粒沙,byte[] 就是那一整箱连续摆好的沙。你不能用「一盒贴了标签的糖果」去表达它,因为标签(int/double)对这批货毫无意义。

示意代码:

csharp 复制代码
int totalBytes = sizeX * sizeY * sizeZ;   // 算出一共多少字节
byte[] raw = new byte[totalBytes];         // 要这么多个字节的连续内存

三、为什么要「钉」住:GC 会「搬家」

分配完 byte[] 还不够。.NET 的 GC(垃圾回收器)会不定时「搬家」------它把还活着的对象挪到一起(内存压缩),腾出连续空间。

问题来了:如果你已经把这块内存的地址 交给了 C++,GC 突然把这块内存搬走了,C++ 手里那个地址就指向了旧位置(可能是垃圾、可能是别的对象)→ 读到错数据甚至崩溃。

所以交地址之前,必须把这块内存钉住(pinning),告诉 GC「这块别动」。

.NET 公开 API 是 GCHandle

csharp 复制代码
// 钉住:告诉 GC 这块内存别动
var handle = GCHandle.Alloc(raw, GCHandleType.Pinned);

// 取固定后的首地址
IntPtr firstAddress = handle.AddrOfPinnedObject();

// 用完解钉:GC 又能管理这块内存了
handle.Free();

三件事,拆开讲语法:

  • GCHandle.Alloc(数组, GCHandleType.Pinned) ------ 第一个参数是你要钉的数组,第二个参数 GCHandleType.Pinned 是「固定」模式。
  • .AddrOfPinnedObject() ------ 返回这块内存的首地址IntPtr 类型)。钉住之后这个地址就固定不变了。
  • .Free() ------ 解钉。用完必须解,否则这块内存一直被钉着,GC 永远搬不动它,等于霸占。

生活类比:GC 像搬家工人 ,会把你家家具重新摆整齐。你在墙上开了个洞(地址)告诉朋友「C++ 从这进」,结果工人把墙搬走了,朋友按旧地址找到的是别家。钉住 = 跟工人说「这面墙别动」Free() = 说「好了,墙可以动了」。


四、怎么「交」:门牌号 = 共享内存

钉住拿到首地址后,怎么把数据「交」给 C++?

关键认知:C++ 不认识 C# 的 byte[] 对象,它只认「内存地址」。所以 C# 把首地址告诉 C++,两边就共享了同一块内存:

复制代码
C# 用 byte[](对象)看它  ⟷  同一块物理内存  ⟷  C++ 用地址(指针)看它

示意代码:

csharp 复制代码
param.volumeAddress = firstAddress;    // 把首地址塞进参数结构体
cppAlgorithm.Run(params);              // 结构体传给 C++,C++ 往这个地址写数据

C++ 拿到地址就往里写体数据;因为地址就是那块 byte[] 的首地址,所以 C++ 写进去 = 写进了 C# 的 byte[]

生活类比:C# 有个空仓库 (byte\[\]),要请 C++ 这个「生产工人」往里放货。但 C++ 只懂「门牌号」(地址),不懂「仓库对象」。于是 C# 把仓库门牌号写在单子上(param.volumeAddress)递给 C++。C++ 拿着门牌号走到那个地址放货------门牌号就是仓库地址,货自然进了 C# 的仓库。

一句话:首地址就是 C# 和 C++ 之间的「门牌号」,让两个世界共享同一块内存。


五、数据的三本账:原料仓库 → 加工车间 → 发货单

体数据在程序里不是一直待在一个地方,它在「三本账」之间接力:

复制代码
 [原料仓库]  ──提取──▶  [加工车间]  ──检测──▶  结果
 (重建算法写进来)     (检测算法从这里取)
              └────────────────────────────────▶ [发货单](只记文件路径)
账本 角色 装什么
原料仓库 重建算法「生产」出来的原始体数据堆这里 体数据本身
加工车间 检测算法「消费」------从这里取体数据去算 体数据本身
发货单 体数据写盘后,只记文件路径 路径字符串,不是数据

两个容易混的点,记牢:

  1. 生产 vs 消费:原料仓库是「生产方」(重建算法写进来),加工车间是「消费方」(检测算法取出去)。别搞反。
  2. 发货单存的是「路径」,不是数据本身。货本身进了磁盘文件,发货单只写「货在 3 号仓 B 区」。

生活类比:这就是一个车间。原料仓库放刚生产出来的货,加工车间来领货加工,发货单登记「货发到哪了」。三本账各司其职,货(体数据)在仓库之间流转。


六、取消机制:源(扳手)vs 令牌(信号线)

一个任务跑到一半要能「安全喊停」。.NET 用两个东西配合,核心是权限分离

能干什么 关键成员
CancellationTokenSource(源) 能「写」------主动触发取消 .Cancel()
CancellationToken(令牌) 只能「读」------只能看取消了没 .IsCancellationRequested

源是扳手,令牌是信号线。 扳手能按(.Cancel()),信号线只能看(查「取消了没」)。

从源上接出令牌,用 .Token

csharp 复制代码
var source = new CancellationTokenSource();   // 造一个源(扳手)
CancellationToken token = source.Token;       // 从源上接出令牌(信号线)

source.Cancel();                              // 按下扳手,触发取消
token.IsCancellationRequested;                // 看:true(取消了)
  • new CancellationTokenSource() ------ 造一个「能按下的取消开关」。
  • .Token ------ 从开关上接出一根「信号线」。
  • .Cancel() ------ 按下开关。
  • .IsCancellationRequested ------ 查「取消了没」。
  • token.ThrowIfCancellationRequested() ------ 取消了就抛异常中断(跑任务的代码常用这句「检查哨」)。

为什么要分两个? 取消权(扳手)集中内部,知情权(信号线)发给外面。外面拿着令牌只能「看」,不能「随便取消你」。

生活类比:报警按钮 (源)只有管理员能按;指示灯(令牌)谁都能看。你不能靠「看灯」去「按按钮」------触发取消必须扳扳手。

进阶:联动开关

有时候一个任务要「既听自己的、又听上级的」。用 CreateLinkedTokenSource 把两个令牌「链接」起来:

csharp 复制代码
// 联动:父令牌 或 自己的令牌,任一取消,这个源都取消
var linked = CancellationTokenSource.CreateLinkedTokenSource(parentToken, selfToken);

生活类比:房间的进线 接了两路------楼里的总闸 (父令牌)和房间的分闸 (自己的令牌)。总闸断分闸断,房间都断电。


附录:这条线的完整链路

复制代码
体数据(体素堆)
   │  用 byte[] 装(无类型裸字节)
   ▼
new byte[totalBytes]
   │  钉住(GCHandle.Alloc + Pinned)------ 防 GC 搬家
   ▼
拿到首地址(门牌号)
   │  交给 C++(结构体传地址)
   ▼
C++ 往地址写 = 写进 C# 的 byte[]
   │
   ▼
三本账接力:原料仓库 → 加工车间 → 发货单
   │
   ▼
全程用「源/令牌」管理取消(扳手按、信号线看)

核心就一句话 :C# 用 byte[] 装裸字节 → 钉住保地址 → 把门牌号交给 C++ 共享内存 → 数据在三本账里接力 → 用「源 vs 令牌」安全喊停。

相关推荐
淡海水2 小时前
03-03-线性-LinkedList-T-源码不变式与选择边界
windows·链表·c#·编译·linkedlist·clr·机器码
longxiaozhang617 小时前
C# Array类:数组其实没那么难
开发语言·c#
longxiaozhang620 小时前
C#运算符重载:让对象也能使用“加减乘除”
开发语言·c#
花落人散处21 小时前
C# 核心编程知识点总结:网络编程、委托与事件、异步编程与LINQ
c#
fl1768311 天前
基于C#WPF实现的内存加速球清理类似360加速球内存清理
开发语言·c#·wpf
CoderIsArt1 天前
OPC UA 完整介绍、官网、C# 简单使用
开发语言·c#
界面开发小八哥1 天前
界面控件DevExpress XAF v26.1新版亮点——AI Agent Skills
ai·c#·跨平台·devexpress·ui开发·xaf
csdn_aspnet1 天前
C# 高效便利的处理数据
开发语言·windows·c#
淡海水1 天前
03-02-线性-List-T-动态数组布局-扩容与操作成本
数据结构·windows·c#·list·编译·clr·机器码