Overview
复习
-
并发编程的基本工具:线程库、互斥和同步
-
并发编程的应用场景:高性能计算、数据中心、网页/移动应用
-
应对 bug (和并发 bug) 的方法
-
死锁和数据竞争
应对bug的方法
把任何你认为 “不对” 的情况都检查一遍
Bug 多的根本原因:编程语言的缺陷
软件是需求 (规约) 在计算机数字世界的投影。
只管 “翻译” 代码,不管和实际需求 (规约) 是否匹配
balance 代表 “余额”
-
- 怎么看 withdraw 以后 0 → 18446744073709551516 都不对
防御性编程
写出验证代码的东西:assert
死锁 (Deadlock)
A deadlock is a state in which each member of a group is waiting for another member, including itself, to take action.
出现线程 “互相等待” 的情况
避免死锁
死锁产生的四个必要条件 (Edward G. Coffman, 1971):
- 互斥:一个资源每次只能被一个进程使用
- 请求与保持:一个进程请求资阻塞时,不释放已获得的资源
- 不剥夺:进程已获得的资源不能强行剥夺
- 循环等待:若干进程之间形成头尾相接的循环等待资源关系
避免死锁 (cont'd)
AA-Deadlock
ABBA-Deadlock
- 任意时刻系统中的锁都是有限的
- 严格按照固定的顺序获得所有锁 (lock ordering; 消除 “循环等待”)
-
- 遇事不决可视化:lock-ordering.py
- 进而证明
T1:A→B→C;T2:B→C 是安全的
-
-
- “在任意时刻总是有获得 “最靠后” 锁的可以继续执行”
-
并发 Bug:数据竞争 (Data Race)
不上锁不就没有死锁了吗?
数据竞争
不同的线程同时访问同一段内存,且至少有一个是写。
- 两个内存访问在 “赛跑”,“跑赢” 的操作先执行
用互斥锁保护好共享数据,来消灭数据竞争
数据竞争:例子
// Case #1: 上错了锁
void thread1() { spin_lock(&lk1); sum++; spin_unlock(&lk1); }
void thread2() { spin_lock(&lk2); sum++; spin_unlock(&lk2); }
// Case #2: 忘记上锁
void thread1() { spin_lock(&lk1); sum++; spin_unlock(&lk1); }
void thread2() { sum++; }
更多类型并发bug
回顾我们实现并发控制的工具
- 互斥锁 (lock/unlock) - 原子性
- 条件变量 (wait/signal) - 同步
忘记上锁——原子性违反 (Atomicity Violation, AV)
忘记同步——顺序违反 (Order Violation, OV)
原子性违反 (AV)
“ABA”
- 我以为一段代码没啥事呢,但被人强势插入了
原子性违反 (cont'd)
有时候上锁也不解决问题
- “TOCTTOU” - time of check to time of use
发邮件,检查是否是一个普通文件的时候,攻击者在文件检查完以后偷偷删掉然后重新创建出一个文件链接(不安全的)发送给接收方
顺序违反 (OV)
“BA”
- 怎么就没按我预想的顺序来呢?
-
- 例子:concurrent use after free
应对并发 Bug 的方法
完全一样的基本思路:否定你自己
始终假设自己的代码是错的。
然后呢?
- 做好测试
- 检查哪里错了
- 再检查哪里错了
- 再再检查哪里错了
-
- (把任何你认为 “不对” 的情况都检查一遍)
Lockdep: 运行时的死锁检查
Lockdep 规约 (Specification)
检查方法:printf
- 记录所有观察到的上锁顺序,例如[x,y,z]⇒x→y,x→z,y→z
- 检查是否存在顺序错误,或者说x->y 并 y ->x
ThreadSanitizer: 运行时的数据竞争检查
为所有事件建立 happens-before 关系图
- Program-order + release-acquire
- 对于发生在不同线程且至少有一个是写的 x,y 检查
更多的检查:动态程序分析
在事件发生时记录
- Lockdep: lock/unlock
- ThreadSanitizer: 内存访问 + lock/unlock
解析记录检查问题
- Lockdep: x⇝y∧y⇝x
- ThreadSanitizer: x⊀y∧y⊀x
付出的代价和权衡
- 程序执行变慢
- 但更容易找到 bug (因此在测试环境中常用)
动态分析工具:Sanitizers
没用过 lint/sanitizers?
- AddressSanitizer (asan); (paper): 非法内存访问
- ThreadSanitizer (tsan): 数据竞争
-
- Demo: fish.c, sum.c, peterson-barrier.c; ktsan
- MemorySanitizer (msan): 未初始化的读取
- UBSanitizer (ubsan): undefined behavior
-
- Misaligned pointer, signed integer overflow, ...
- Kernel 会带着 -fwrapv 编译
以上这些就是防御性编程
我们也可以!Buffer Overrun 检查
Canary (金丝雀) 对一氧化碳非常敏感
- 用生命预警矿井下的瓦斯泄露 (since 1911)
计算机系统中的 canary
- “牺牲” 一些内存单元,来预警 memory error 的发生
-
- (程序运行时没有动物受到实质的伤害)
防御性编程:低配版 Lockdep
不必大费周章记录什么上锁顺序
- 统计当前的 spin count
-
- 如果超过某个明显不正常的数值 (1,000,000,000) 就报告
int spin_cnt = 0;
while (xchg(&locked, 1)) { if (spin_cnt++ > SPIN_LIMIT) {
printf("Too many spin @ %s:%d\n", __FILE__, __LINE__);
}
}
- 配合调试器和线程 backtrace 一秒诊断死锁
总结
- 常见的并发 bug
-
- 死锁、数据竞争、原子性/顺序违反
- 不要盲目相信自己:检查、检查、检查
-
- 防御性编程:检查
- 动态分析:打印 + 检查