并发 bug 最难缠。它不报编译错误,测试里也未必重现,却能在生产环境的某个凌晨三点精准炸掉服务。数据竞争(data race)尤其如此——两个线程同时碰相同的内存,至少一个是写操作,中间没任何同步。结果?不可预测的崩溃、数据损坏,或者莫名其妙的值。
一个最简例子:
int counter = 0;
void worker() {
for (int i = 0; i < 100000; ++i) {
++counter; // 数据竞争
}
}
int main() {
std::thread t1(worker);
std::thread t2(worker);
t1.join();
t2.join();
std::cout << counter << '\n';
}
你期望输出 200000,但实际可能打印 197423 甚至 199998。这取决于 CPU 缓存、调度时机、优化级别……Debug 模式可能正常,Release 模式就崩,加个日志反而好了——这就是典型的海森堡 bug(Heisenbug)。
Thread Sanitizer(TSan)就是用来抓这种问题的。它是在程序运行时动态检测数据竞争和同步错误的工具,属于编译器内置的 sanitizer 家族一员(Clang/GCC 都支持)。
TSan 工作原理
TSan 依赖四件事:
- 代码插桩:编译时在每次内存访问、锁操作前插入监控代码。
- happens-before 建模:运行期维护事件间的逻辑顺序,判断谁在前谁在后。
- 影子内存:为真实内存的每个区域维护一份元数据,记录'最后是谁、什么时候、读写权限是什么'。
- 竞争判定:如果发现两个线程访问同一内存位置,至少一次是写,且没有 happens-before 关系,立即报警。
所谓 happens-before,是并发正确性的数学根基。同一线程内的语句天然有序;锁的 unlock 发生在之后任何同锁的 lock 之前;特定内存序的原子操作也会建立 happens-before 关系。TSan 里每个线程维护一个向量时钟,每次同步事件就会合并或推进这些时钟。简单说,像这样加锁的代码就不会报 race:
std::mutex m;
int x = 0;
// Thread A
m.lock();
x = 42;
m.unlock();
m.();
std::cout << x;
m.();

