归档异步:GC保活不等于工作线程可以操作UObject
自动关联目录:归档异步:GC保活不等于工作线程可以操作UObject
核心判断:
FGCObjectScopeGuard只保证作用域内对象不会被 GC 回收,不保证捕获它的排队空窗安全,也不赋予 UObject 工作线程访问权,更不保证同一归档句柄能并发读写。
一、业务冲突:压缩必须离开 Game Thread,归档状态又不是无状态函数
创作内容导入、导出会扫描目录、读写文件并压缩大量字节,放在 Game Thread 会直接形成长帧。工程因此把批量添加和解包提交到高优先级后台任务,并把进度、结果重新派发到 Game Thread。这一方向正确,问题在于后台任务操作的不是独立 Job,而是同一个 UObject 内的有状态归档器。
派生实现持有原生压缩句柄或流对象,并维护模式、位置、游标、剩余长度和 Finalize 状态。它们共同描述“一次打开的归档会话”,天然要求单写者或串行访问。
二、实际实现:回调线程处理正确,工作所有权仍混在 UObject 上
调用入口先检查归档器已初始化,然后把原始 this 捕获进任务队列;任务真正开始时才创建 GC Scope Guard,随后直接调用 UObject 虚函数读写内部归档句柄。失败、进度和最终结果再回到 Game Thread。BeginDestroy 则会关闭并重置同一份归档状态。
值得保留的有三点:重 IO/压缩已离开 Game Thread;动态委托不在工作线程执行;批量操作有明确的成功、失败与进度出口。
三、真正的失败边界
- 排队空窗:任务捕获原始指针后,到 Lambda 开始并创建 Guard 前仍有时间差。Guard 不能倒推保护已经排队的裸指针。
- 保活不是线程安全:即使对象始终存在,工作线程仍在调用 UObject 方法并访问可变原生句柄;另一任务、显式 Close 或销毁流程可同时 Reset 同一状态。
- 一个对象没有操作身份:两次批量添加可以并行改写同一流,取消第一笔也无法只撤销第一笔。
- 回到 Game Thread 也不等于可以提交:错误转发再次捕获原始
this;结果没有 OperationId,无法判断调用方是否已关闭、换 World 或发起了新操作。
所以这里不是“再加一把 UObject 锁”就够了。真正缺失的是独立工作状态和操作生命周期。
四、目标设计:UObject 只编排,纯 C++ Job 独占原生句柄
struct ArchiveJob : TSharedFromThis<ArchiveJob, ESPMode::ThreadSafe> {
OperationId id;
TAtomic<bool> cancelled = false;
UniqueArchiveHandle handle; // 只由该 Job 的执行器访问
ImmutableArchivePlan plan;
};
OperationId StartArchive(UObject* owner, ArchivePlan draft) {
check(IsInGameThread());
auto job = MakeShared<ArchiveJob, ESPMode::ThreadSafe>(Freeze(draft));
CommitPermit permit = Scopes.Issue(owner, job->id);
Jobs.Add(job->id, job); // 调度前先建立所有权
SerialExecutor.Enqueue([job, permit] {
ArchiveResult result = RunPureCpp(*job);
GameThread::Enqueue([result = MoveTemp(result), permit] {
if (!Scopes.CanCommit(permit)) return;
Complete(result);
});
});
return job->id;
}每个 Job 独占句柄,或者同一归档 Session 使用串行执行器;后台层只处理值类型计划和纯 C++ 状态,不反向调用 UObject。关闭时先撤销提交权并设置取消标记,再等待 Job 释放原生资源;不可中断的压缩允许完成当前块,但结果不得触碰旧调用方。
五、取舍与验证
独立 Job 会复制文件列表和少量配置,串行化同一归档也会降低单会话并发度;换来的是句柄不共享、取消可定位、销毁可等待。真正需要并行时,应并行不同归档或文件预读,不能并行改写同一有序流。
故障注入至少覆盖:提交后立即 GC、提交后立刻 Close、同一对象并发两次 Add、读写交叉、World teardown、错误回调晚到、取消与完成同帧竞争。指标看排队时间、压缩时间、Game Thread 提交耗时、取消响应、并发拒绝数和关闭等待时间。核心不变量是:任何工作线程写入都只属于一个 Job,任何 Game Thread 提交都能证明该 Job 仍是当前操作。