Supercompute / Infra · 15-minute Technical Phone Screen

xAI BankAccount 并发 Deposit:读改写丢失更新与封装锁

15 分钟技术筛选给 500、700 两线程存款,原 deposit 的 read/sleep/write 可得 700;明确锁范围、继承限制与 Go embedding 陷阱。

题目重点:15 分钟技术筛选给 500、700 两线程存款,原 deposit 的 read/sleep/write 可得 700;明确锁范围、继承限制与 Go embedding 陷阱。

题意与假设

这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。

  • 训练版初始余额 0,金额非负,单进程共享一个 account 对象;所有存款和余额读取都经同一个同步入口。
  • Python 方案允许子类覆盖 deposit 并使用实例 Lock;锁覆盖完整父类 deposit 调用,包含原实现中的 sleep。
  • Go 方案若用组合/embedding 包装旧账户,不能让调用者绕过包装器直调旧对象方法;如果旧类自身暴露给其他线程,包装器锁无法提供全局安全。

解题路线

  1. 画交错:线程 A 与 B 都读取 0,分别算 500、700;A 写 500、B 写 700,最终 700(反过来可为 500),丢失一次更新。sleep 只扩大窗口,不是竞态根因。
  2. 若可改原类,把锁放在 account 对象上,deposit 的 read→compute→write 都在同一临界区;余额读取的公开 API 也用同一锁。
  3. 若 Python 原类不可改,可建 SafeBankAccount(BankAccount),在 __init__ 创建独立 Lock,覆盖 deposit:with lock: super().deposit(amount)。若父类内部回调覆写方法或已有锁,需检查重入;外部也不能直接改 balance。
  4. Go 无类继承;用私有 inner *BankAccount 加 sync.Mutex 的包装 struct,公开 Deposit 与 Balance,并只导出包装器。匿名 embedding 可能把不安全的旧 Deposit 提升为可调用方法,因此组合更容易封住。
  5. 并发测试用 barrier 强制两个 worker 在读后相遇,证实旧实现丢更新;新实现重复多次检查最终 1200 与所有读写入口。一次碰巧得到 1200 不是修复证明。

手动走一遍

旧实现:A 读 0 算 500,B 读 0 算 700;两者 sleep 后依次写,最终 700。加同一实例锁后 A 完成写 500 才让 B 读,B 算 1200 并写回;若 B 先执行也同样 1200。

规模与取舍

单次 deposit 计算 O(1),临界区时间包含原有 0.1s sleep,因此两个操作串行至少约 0.2s;若能重构,把非共享工作移出锁可缩短持锁,但读改写必须仍原子。空间 O(1) 锁状态。

易错点与追问

容易失分的边界

  • 只给读取加锁,写入放在锁外,仍可丢更新。
  • 两个线程各创建自己的锁,无法互斥同一个账户。
  • Python GIL 不保证跨 sleep 的读改写原子性。
  • Go 匿名 embedding 仍暴露旧不安全方法,调用者能绕过包装器。
  • 把作者对 Go embedding 的猜测当成已确认题干。

继续深挖

  • 多个账户之间转账如何避免双锁死锁?
  • 如果余额存数据库,进程内锁为何不足?
  • 如何用版本 CAS 或数据库事务实现?
  • 原方法持锁 sleep 很慢时怎样安全重构?
✓已复制微信号!