数据竞争(Data Race)指两个线程无同步地访问同一内存且至少一个是写;死锁(Deadlock)指线程互相持有对方需要的锁,全体僵持。并发缺陷是最接近"完美犯罪"的一类:证据转瞬即逝、加观测即消失、复现概率随负载波动。本节给出这两类案件的作案机理与专用取证仪器。
一段有竞争的代码长什么样:
// 两个线程同时执行 if (counter == MAX) { // 线程 A 和 B 同时读到 counter == MAX-1 reset_cache(); // 两个线程都进来了:函数被执行两次 } counter++; // 丢失更新:两词自增只生效一次
它狡猾在三处:
编译器和 CPU 还会"协同作案":指令重排让无同步的读写顺序在别的核心眼里与代码顺序不同。所以竞争的定义里强调"无同步"——只要正确用了锁或原子操作,重排就被禁止,案情就不成立。
死锁成立需要四个条件同时满足:互斥、持有并等待、不可剥夺、循环等待。破案的抓手通常是破坏"循环等待"——全员按固定顺序拿锁,环就构不成。
死锁的取证反而简单,因为现场不会消失:死住的程序永远停在那里。第 5.1 节的口供总录直接定罪:
(gdb) thread apply all bt
典型案卷:
Thread 2: holding lock A, waiting lock B #0 futex_wait ... mutex_lock (lock=B) #1 withdraw (account=...) at transfer.c:44 Thread 5: holding lock B, waiting lock A #0 futex_wait ... mutex_lock (lock=A) #1 deposit (account=...) at transfer.c:61
两个线程的回溯交叉成环,各自持有的锁和等待的锁白纸黑字——死锁案是并发案件里最好破的。
运行时检测还有更主动的手段。pthread_mutex 有错误检查模式(PTHREAD_MUTEX_ERRORCHECK),能查出同线程重复加锁这类自锁;更强的锁分析器能画出等待图并自动找环。
ThreadSanitizer 是编译器内建的插桩工具(-fsanitize=thread),在每次内存访问处插入检测代码,用向量时钟算法(类似 Lamport 时钟)判断两次访问是否构成无同步的冲突。它的可怕之处在于不依赖复现出错结果——交错本身被记录,即使这次没造成损害也报警:
# 构建时开启(重编译,不能事后挂载) clang -fsanitize=thread -g -O1 -o myapp myapp.c ./myapp
报警输出:
WARNING: ThreadSanitizer: data race Write of size 8 at 0x7f... by thread T2: #0 update_stats stats.c:120 Previous read of size 8 at 0x7f... by thread T5: #0 render_dashboard ui.c:88 Location is global 'stats_buffer'
一次读写双方、两份栈、冲突地址全齐——把 stats.c:120 的写加锁或改原子,案子就破。TSan 的代价:内存涨 5–8 倍、速度慢 2–20 倍,所以它属于测试环境仪器:压测流量回放时开着它跑,把交错摊开给检测器看。

⚠️ 常见坑:用 TSan 和 ASan 同时插桩。两个 sanitizer 的运行时互不兼容,会直接崩溃或误报满天飞——一次只开一种。
取证的尽头是预防。行之有效的三道防线:
死锁是最"友好"的并发案:现场凝固不动,取证只要把每个线程"拿着什么、等什么"抄下来:
gdb -p $PID -batch \ -ex 'set print thread-events off' \ -ex 'thread apply all bt 8' > deadlock.txt # 人工比对: 线程A 栈中 lock(m1)->lock(m2), 线程B 栈中 lock(m2)->lock(m1) # => 锁序反转, 交叉等待即铁证 # 运行期监测: glibc 内置死锁检测 export LD_PRELOAD=libstdc++... # 或使用 hb_deadlock/ 当场打印持有矩阵
写成报告时附上"锁获取顺序图",工程修复是给全局锁定序(如按地址排序加锁),并把它写进团队规范——死锁的防复发手段只有一条:全系统统一的锁序。
ThreadSanitizer 报告的核心是两栏:一栏写操作(读/写、栈、持有锁),另一栏写先前冲突操作。定性看类型:data race(同一地址并发读写且无同步)是未定义行为的铁证;deadlock 是锁环警告。TSan 的开销约 5–15 倍减速与 5–10 倍内存,只能进测试负载,配合压力并发与随机调度扰动(TSAN_OPTIONS=halt_on_error=0 history_size=7)在 CI 里长期跑,比人肉 review 并发补丁可靠得多。
clang++ -fsanitize=thread -g -O1 race_demo.cpp -o demo TSAN_OPTIONS=second_deadlock_stack=1 ./demo 2>&1 | head -40