← ALL ARTICLES

帧分配器的构造与退休

全库目录221
本文目录7

一批4个粒子的位置与速度各占64字节,年龄占16字节。CPU生成这144字节SoA源数据,再打包为GPU消费的192字节AoS。分配好存储后,辅助对象还需要按依赖构造,CPU任务与GPU队列全部结束以后才能析构和复用。本文给出构造登记、依赖借用和帧槽退休协议;字节布局、对齐与线性游标见内存布局与分配。示例对象A的析构需要对象B,三个帧槽按实际队列完成值轮换。

构造登记与提交次序

存储可用了,辅助对象还需要构造。设对象B保存粒子统计缓冲,对象A保存上传描述,A的析构函数还要向B写回最终统计。两个任务可能先为A预留存储,再为B预留存储,B却先完成构造。按照预留槽的逆序析构会先销毁B,使A的最后一次访问悬空。

这里为每代分配区保存独立的构造记录表与提交链。记录状态为Reserved、Committed、Aborted或Destroyed,句柄携带槽位、批次代次和记录身份。任务先持有批次引用,在记录锁内预留记录槽与依赖列表空间,再领取对象存储。任何容量不足都在调用构造函数之前返回失败;多线程的失败预留保留字节空洞到整批回收,避免回退游标覆盖后来分配的对象。

构造函数在锁外执行。它取得的同批依赖必须已经Committed,尚在Reserved中的对象由任务调度建立前置关系,等其提交后再构造依赖者。构造期间对象指针只归本任务使用,成功提交以前不向消费者发布。构造抛出异常时,C++释放已完成构造的成员与基类,外层记录置Aborted;外层对象没有完成构造,记录表不会再次调用它的析构函数。裸资源由构造过程中的RAII临时对象持有,防止失败路径泄漏。

构造成功后,任务在同一记录锁内校验本代身份和依赖记录,分配下一个提交序号,将对象指针、析构函数和依赖列表写入预留槽,并把槽接到提交链尾部。链节点已经预留,写入与连接均不分配内存;序号空间也在开始新构造前检查,批次记录容量限制保证序号可表示。提交完成才发布句柄,最后归还任务引用。Closing允许已持引用的构造任务提交,退休过程等待其退出,因此状态关闭不会在对象构造成功后制造一次登记失败。

本例中B先提交得到序号1,A随后提交得到序号2;清理按提交链逆序执行A、B。两个无依赖对象并发完成时,由提交锁确定唯一顺序。这里的逆序是分配器定义的提交次序,与存储预留次序及CPU指令完成时刻分别记录。

析构依赖与变更检查

提交后增加析构借用也经过记录接口:若A析构要读取B,登记边A→B,要求两者同代、均Committed且sequence(A)>sequence(B)。构造时的依赖自动满足此条件,提交后的新边在写入对象字段前检查;元数据空间不足或顺序不符时,字段保持原值。所有边都指向更小序号,逆序析构就会在销毁A时保留B,并且不会形成环。访问记录表与发布字段之间使用同一对象同步协议,正在运行的消费者只能看到变更前或变更后的完整关系。

若B后来也要在析构时读取A,接口会拒绝B→A。业务层可在CPU和GPU消费者退出后执行显式收尾,把双向关联结算为各自独立的值,再在记录锁内清除借用边与对应字段;收尾需要失败时,批次停留在Closing并保留存储,记录已完成的收尾步骤后重试。也可把互相依赖的对象交给一个组合所有者,内部明确安排关闭与销毁顺序。外部对象则通过独立所有权令牌保持到本批析构完成,跨代裸指针无法提供这份保证。

本分配区由批次统一管理对象销毁。消费者释放句柄只归还自己的借用引用,对象保持Committed到退休执行者处理;调用者通过批次接口请求收尾,实际析构始终由该执行者独占完成。需要独立提前销毁的对象使用另一个具备对象关闭状态和借用排空接口的所有者,交接时将数据复制或移动构造到独立存储,建立新所有者的持有后再释放批次借用;原分配区中的移后对象仍随批次析构,详见插件接口与对象借用。

收尾与析构对依赖有不同要求。显式收尾可以等待异步结果,完成状态逐项登记,失败时保留Closing及全部存储;进入逆序析构以后只执行同步、不会失败的资源清理。B即使已停止业务更新,其统计缓冲仍保留到A析构写回并结束,随后才轮到B。外部模块提供这些资源时,批次先向宿主登记代表本批的拥有者与提供者依赖边,取得关闭保留与清理权限后才发布A的句柄;这些持有持续到A清理返回,随后注销批次拥有者与依赖边。清理调用按插件关闭与卸载完成;普通Active入口关闭不会使已有析构依赖失去合法访问路径。

批次析构函数要求不抛异常,且只访问已登记依赖或自身资源;异步写入和可能失败的业务结算放在上述收尾阶段。退休执行者独占遍历提交链,逐个调用析构并标记Destroyed,期间保留全部对象存储和记录表,最后统一清空。若程序违背不抛异常约定,进入故障处理而不重新执行已开始的析构;它无法假设部分执行的析构可以安全重试。只清游标则会跳过文件句柄、堆缓冲等成员资源释放。

异步批次与复用

源数组已经更新、192字节AoS也已打包,复制队列却可能仍在读取上传区。以三个帧槽位为例,第f帧使用slot=f%3。每个槽位记录游标、代次、CPU任务引用计数、构造记录表,以及各GPU队列实际提交时返回的完成值。槽位从Open进入Closing后等待CPU计数归零,再确认各队列已完成值不小于该槽提交值,完成可能失败的显式收尾,最后按提交链逆序析构。全部析构结束才递增代次、清游标并重新开放。GPU提交完成值来自实际提交;帧号不能证明GPU已经读完。

例如本批记录图形队列值41、复制队列值17,当前完成值为42和16,即使A、B的构造任务都退出,A仍不能析构;复制队列达到17后才完成消费者排空,再按序号2、1销毁A、B。旧句柄上的代次仍为上一批,新的领取接口先检查代次,拒绝访问复用后的对象。

领取外部任务引用、关闭入口与观察CPU计数归零在同一同步门内完成。Closing关闭外部领取;已持引用的任务仍可生成子任务,但先增加子引用再释放父引用。构造登记和GPU提交值也在任务释放最后引用之前完成。多次提交以同步的最大值保存同一队列最后需要等待的完成值,防止迟到的小值覆盖大值。消费者排空以后析构阶段禁止创建本批任务或GPU提交,才能使排空条件在清理期间保持成立。

跨批次数据复制到长寿命存储并改写引用;若必须借用本批对象,长期消费者继续持有引用,槽位保持Closing,调度器选择另一个可用槽位或等待。GPU队列故障无法确认完成时也保留该槽,直到设备恢复流程证明旧提交停止访问。完成值、等待与可见性细节见 GPU资源生命周期。

验证方法

让B先构造提交、A后提交,再按A、B顺序析构;改变存储预留次序,结果仍按提交序号决定。让复制队列完成值停在16而所需值为17,确认CPU任务结束后仍保留整批。随后检查Closing期间的构造提交、子任务交接、析构借用逆序、失败收尾和设备故障。

实现与证据

本文定义帧分配器的宿主协议,构造序号1/2与图形41、复制17的例子为状态推演。尚未实现并发C++分配器、GPU提交、异步收尾或故障恢复。内存布局与分配中的五项整数检查只支持字节对齐与容量边界,不覆盖本页构造和退休协议。

相关主题

参考资料