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 的主路径通常包含:
- 用户态发起 syscall
- VFS 路由到具体文件对象
- page cache/buffer cache 命中检查
- 命中则内存拷贝;未命中触发块设备 IO
- 返回用户态
要点:
- IO 慢不只因为“磁盘慢”,还叠加 syscall、缓存行为、拷贝、锁竞争。
- 顺序访问与随机访问对 page cache 效率差异巨大。
- fsync/刷盘语义会显著改变延迟分布(尤其尾部)。
IO 与并发的耦合坑
- 多线程并发小 IO:可能把设备队列和内核锁打满,吞吐提升有限。
- 过度同步日志/持久化:把应用延迟锚定到存储尾延迟。
- 单全局队列:容易形成锁热点与 head-of-line blocking。
面向低延迟的落地策略
- 控制共享写热点:分片 + 局部汇总。
- 关键路径减少阻塞锁;把慢操作移出临界区。
- 对 IO 采用批量提交与合并刷盘策略(在一致性允许范围内)。
- 先用 profile 找 contention 点,再决定是否 lock-free。
最后你应形成的判断力
- 看到锁争用时,先问“临界区太长”还是“并发模型错位”。
- 看到 IO 抖动时,先拆“边界成本/缓存命中/设备尾延迟”。
- 优化时优先消除系统性排队,再做局部指令级优化。