启用tsan必须在编译和链接阶段均添加-fsanitize=thread,配合-g、-o1、-pthread;clang支持更成熟,禁与asan混用;报错含精确内存地址、线程编号及源码行号。

用 -fsanitize=thread 编译就能启用 TSan
ThreadSanitizer 不是运行时开关,必须在编译阶段加入特定标志,否则链接或运行时都看不到竞争检测。GCC 和 Clang 都支持,但 Clang 的实现更成熟、误报更少,推荐优先用 clang++。
最小可运行命令示例:
clang++ -O1 -g -fsanitize=thread -pthread main.cpp -o main
关键点:
-
-fsanitize=thread必须同时出现在编译和链接阶段(即所有.cpp文件和最终链接命令都要带上) -
-pthread不能省——TSan 依赖 pthread 符号做拦截,漏掉会导致链接失败或运行时报undefined symbol: __tsan_mutex_create -
-O1是底线,-O2及以上可能因优化隐藏某些竞争路径,导致漏报;但-O0会显著拖慢 TSan 运行速度,且部分内存访问模式在无优化下不触发检测 - 不要混用
-fsanitize=address和-fsanitize=thread——二者 runtime 冲突,会直接崩溃
报错信息里怎么看哪两个线程在抢什么
TSan 报错不是简单抛异常,而是输出带堆栈的文本报告,核心信息藏在 Previous write / Current write 或 Previous read / Current write 块里。
典型片段:
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
WARNING: ThreadSanitizer: data race (pid=1234)
Write of size 4 at 0x7b0c00000020 by thread T2:
#0 main::thread_func() /tmp/main.cpp:12 (main+0x4e)
Previous write of size 4 at 0x7b0c00000020 by thread T1:
#0 main::thread_func() /tmp/main.cpp:12 (main+0x4e)
Location is global 'counter' of size 4 at 0x7b0c00000020 (main+0x000000000020)
重点抓三处:
-
Write of size 4 at 0x...—— 出问题的内存地址,后面跟着global 'counter'表明是全局变量;如果是heap block就说明是 new/malloc 出来的对象 -
by thread T2和Previous write ... by thread T1—— 明确告诉你哪两个线程(T1/T2 是 TSan 自动编号)在并发访问同一地址 - 两段堆栈末尾的
/tmp/main.cpp:12—— 竞争发生的精确行号,注意不是“谁加锁错了”,而是“谁没加锁就写了”
为什么加了 mutex 还报竞态?常见漏点
TSan 能识别标准同步原语(std::mutex、pthread_mutex_t、std::atomic),但对保护范围极其敏感。以下情况即使写了锁,TSan 仍会报错:
- 锁对象本身是局部变量或未正确初始化(比如
pthread_mutex_t m;没调pthread_mutex_init(&m, nullptr)) - 读写不同变量却共用一个锁——TSan 不检查逻辑合理性,只看实际访问地址是否被同一线程序列化;如果两个线程分别操作
a和b,但只锁了a,b仍裸奔 - 用了
std::shared_mutex却混用lock()和lock_shared(),而读写操作没对应上——TSan 认为共享锁不阻止写,所以写操作必须用独占锁 - 跨 DLL/so 边界传递裸指针并并发访问——TSan 在模块边界可能丢失锁上下文,尤其 Windows 下 DLL 导出类成员函数时容易漏锁
怎么让 TSan 报得更准、跑得更快
默认行为偏保守:开启所有内存访问拦截,但代价是 5–10 倍性能下降、GB 级内存占用。日常排查可微调:
- 用环境变量
TOTAL_MEMORY_LIMIT_MB=2048限制 TSan 自身内存,避免 OOM(默认不限) - 加
-fPIE -pie编译位置无关可执行文件,能提升 TSan 对动态链接符号的拦截准确率 - 若只关心某几个源文件,可用
-fsanitize-blacklist=tsan.ignore配合黑名单文件屏蔽第三方库(内容写如src/third_party/.*),减少干扰 - 遇到误报(比如确定有锁但 TSan 没识别),可临时加
__attribute__((no_sanitize("thread")))标记函数跳过检测——但这是掩耳盗铃,应优先查锁是否真生效
真正难的是那些非显式共享:static 局部变量、单例内部状态、TLS 变量被错误地当成全局来用。TSan 能揪出它们,但需要你一眼看出 “这个 static 怎么可能被两个线程同时初始化”。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!










