Hang Zhengyang

Week 4:Lock / Concurrency + File I/O Path(高密度版)

一个总公式

并发性能退化通常可写成:

总时间 = 有效工作 + 同步开销 + 等待开销 + 失配开销(cache/NUMA/调度)

很多系统“看起来在并发”,其实在做同步与等待。

锁的本质:序列化点,不是原罪

锁慢不慢取决于竞争形态:

  • 低竞争:锁开销接近常数,通常可接受。
  • 高竞争:排队 + 失去局部性 + 调度干预,代价非线性增长。

因此“lock != 慢,contention 才慢”是工程事实,不是口号。

Spinlock vs Sleep Lock:选择依据

Spinlock

  • 适合临界区极短、不可睡眠上下文。
  • 风险:持锁时间超预期会烧 CPU,并放大总线争用。

Sleep Lock / Mutex

  • 适合临界区较长或可能阻塞。
  • 风险:sleep-wakeup 路径引入调度抖动与优先级反转问题。

经验判断:

  • 临界区可控且短:偏 spin
  • 临界区不确定或可能阻塞:偏 sleep/mutex
  • 先测持锁分布,再定策略,不凭感觉。

Memory Ordering:你至少要守住的边界

多核可见性问题不是“编译器玄学”,而是硬件重排 + 缓冲导致:

  • 原子操作保证单点原子性,不自动保证复杂时序正确。
  • acquire/release 决定“哪些写在谁之前可见”。
  • 锁本身通常隐含必要屏障,但 lock-free 结构必须显式建模 happens-before。

实战原则:
在低延迟系统里,宁可先用可证明正确的锁方案,再在热点处局部无锁化。

False Sharing:最隐蔽的吞吐杀手之一

不同线程写不同变量,只要落在同一 cache line,仍会互相 invalidation。
症状:CPU 忙、业务逻辑简单、扩线程后吞吐不升反降。

处理:

  • 热写字段按 cache line 对齐/填充
  • 减少跨核写共享状态
  • 使用 per-core/per-thread 分片计数,后聚合

File I/O 路径(只看关键链路)

一次 read/write 的主路径通常包含:

  1. 用户态发起 syscall
  2. VFS 路由到具体文件对象
  3. page cache/buffer cache 命中检查
  4. 命中则内存拷贝;未命中触发块设备 IO
  5. 返回用户态

要点:

  • IO 慢不只因为“磁盘慢”,还叠加 syscall、缓存行为、拷贝、锁竞争。
  • 顺序访问与随机访问对 page cache 效率差异巨大。
  • fsync/刷盘语义会显著改变延迟分布(尤其尾部)。

IO 与并发的耦合坑

  • 多线程并发小 IO:可能把设备队列和内核锁打满,吞吐提升有限。
  • 过度同步日志/持久化:把应用延迟锚定到存储尾延迟。
  • 单全局队列:容易形成锁热点与 head-of-line blocking。

面向低延迟的落地策略

  • 控制共享写热点:分片 + 局部汇总。
  • 关键路径减少阻塞锁;把慢操作移出临界区。
  • 对 IO 采用批量提交与合并刷盘策略(在一致性允许范围内)。
  • 先用 profile 找 contention 点,再决定是否 lock-free。

最后你应形成的判断力

  • 看到锁争用时,先问“临界区太长”还是“并发模型错位”。
  • 看到 IO 抖动时,先拆“边界成本/缓存命中/设备尾延迟”。
  • 优化时优先消除系统性排队,再做局部指令级优化。