后台写盘:线程安全队列不等于可关闭管线

自动关联目录:后台写盘:线程安全队列不等于可关闭管线

核心判断:互斥锁只能保护一次入队或出队;可靠后台写盘还必须回答何时拒绝新数据、停止时写完还是丢弃、怎样唤醒等待者、何时可以释放线程对象。

一、业务冲突:操作记录不能卡主线程,也不能在退出时悄悄丢失

创作操作和脚本诊断会持续追加记录。同步打开、编码和写盘会放大 Game Thread 抖动,因此工程实现了后台写线程,并出现过两种形态:一种按记录入线程安全队列,逐条追加;另一种按文件聚合字符串,在一个临界区内批量编码、一次写入。后者减少了文件调用次数,这是有效的吞吐优化。

二、实际实现:数据竞争被部分解决,关闭协议没有建立

两种实现都把生产者与文件 IO 分开,也都用锁保护共享容器。但完整控制流仍有共同问题:

  • 运行和暂停标记是跨线程读写的普通 bool,没有原子或同一把锁保护。
  • 消费者通过固定时长 Sleep 轮询;新数据无法立即唤醒,空闲时又持续增加关闭延迟。
  • 暂停同时使用业务标记和 OS 线程 Suspend;一种 Run 路径甚至把“暂停”解释为线程退出,之后 Wake 无法让已结束的 Run 重新开始。
  • 停止依赖 Kill(true)。一种变体随后删除线程对象,另一种只把工作对象指针置空;两者都没有清晰证明正常退出和资源唯一释放。
  • 停止时没有统一的 Drain 语义。有的队列直接清空,有的在 bRunning=false 后不再消费;已经返回“入队成功”的记录可能未落盘。
  • 批量版本持锁完成编码和文件写入,慢盘会让所有生产者在同一临界区排队。

这说明“批量写”优化了平均 IO,却没有解决生命周期正确性。

三、目标设计:Close、Drain/Abort、Join 是一个状态机

enum class WriterState { Running, ClosingDrain, ClosingAbort, Stopped };

bool Enqueue(Record r) {
    Lock guard(mutex);
    if (state != Running) return false;
    if (queuedBytes + r.Bytes() > maxQueuedBytes) return false;
    queue.Push(MoveTemp(r));
    queuedBytes += r.Bytes();
    event->Trigger();
    return true;
}

uint32 Run() {
    for (;;) {
        event->Wait();
        Batch local;
        {
            Lock guard(mutex);
            queue.SwapInto(local);     // IO 不持生产者锁
            if (state == ClosingAbort) local.Reset();
            if (state != Running && local.IsEmpty() && queue.IsEmpty()) break;
        }
        EncodeAndAppendGroupedByFile(local);
    }
    state = Stopped;
    return 0;
}

void Shutdown(ShutdownPolicy p) {
    TransitionRunningToClosing(p);    // 此后 Enqueue 必须失败
    event->Trigger();                 // 唤醒空闲消费者
    thread->WaitForCompletion();      // 只 Join,不 Suspend/Kill
}

正常退出选择 Drain:停止接收,写完已接受记录,再 Join。崩溃止损或磁盘不可用可选择 Abort,但必须返回丢弃条数与字节,不能伪装成成功。队列按字节设上限;达到高水位时由业务选择拒绝低价值记录、同步关键记录或短暂背压。

四、批量策略的正确边界

按文件聚合能减少打开和追加次数,但批次必须在锁内快速交换、锁外编码写入。同一路径需要保持 FIFO;不同文件可并行,但要限制同时打开的文件数。写入失败不能无限重试占满内存,应按错误类型执行有限重试、降级路径或明确失败。

若记录用于恢复或审计,还需定义 Flush Fence:调用方拿到 fence 后,可以等待“此前所有记录已持久化”。仅仅观察队列为空不够,因为消费者可能已经取走但尚未写完。

五、决策与验证

压测生产速度高于磁盘速度、暂停时持续入队、空队列关闭、满队列关闭、写入中断电、文件权限失败和重复 Start/Stop。指标包括排队字节、最老记录年龄、批量大小、每文件写入次数、生产者阻塞、拒绝/丢弃量、Flush 延迟和 Shutdown 延迟。

判断后台写盘成熟度,不看“有没有线程安全队列”,而看:一个已被接受的记录,最终只有三种可观测结局:已持久化、明确失败、按声明策略被丢弃。