1. 概述
1.1 什么是 TSan
ThreadSanitizer(TSan) 是一种数据竞争检测器(Data Race Detector)。它能在程序运行时发现多个线程/ goroutine 对同一块内存的无同步并发访问(即 data race),从而帮助定位难以复现的并发 bug。
- Go:官方内置的 race detector 基于 TSan 思想实现,通过
go build -race启用。 - C/C++:LLVM/Clang 和 GCC 提供
-fsanitize=thread(ThreadSanitizer),在编译和链接时加入检测代码。
1.2 什么是数据竞争(Data Race)
当以下三个条件同时满足时,称为发生数据竞争:
- 至少两个线程/ goroutine 访问同一块内存;
- 其中至少有一个是写操作;
- 这些访问之间没有通过同步原语(如 mutex、channel、atomic)建立明确的先后顺序。
数据竞争会导致未定义行为:可能表现为偶发崩溃、错误结果或难以理解的 bug,因此需要在测试与开发阶段尽量排除。
2.1 总体思路
TSan 不直接分析“是否正确加锁”,而是通过记录每次内存访问,在运行时检查是否存在“两个不同线程的访问在逻辑上并发且至少有一个写、且无同步关系”的情况。核心依赖两点:
- 编译期插桩:在每次内存读/写处插入检测代码。
- 运行时与 shadow state:维护“逻辑时间线”和每个字节的访问历史,用于判断是否构成 race。
- 编译器在每次内存读前插入类似
__tsan_read(addr, size) 的调用。
- 在每次内存写前插入类似
__tsan_write(addr, size) 的调用。
- 在同步原语的进入/退出处插入
__tsan_acquire / __tsan_release 等,用于建立“happens-before”关系。
__tsan_read(addr, size) 的调用。__tsan_write(addr, size) 的调用。__tsan_acquire / __tsan_release 等,用于建立“happens-before”关系。这样,运行时的每次访存和同步事件都会被 TSan 运行时看到。
2.3 Shadow State(影子状态)
TSan 为程序中的内存维护一份“影子”元数据(shadow memory),用来记录:
- 同一线程内,程序顺序定义先后;
- 同步原语在不同线程间建立先后:例如 mutex 的 unlock happens-before 后续的 lock,channel 的 send happens-before 对应的 receive。
当新的访问到来时,TSan 将当前访问与影子中记录的访问做比较:若来自不同线程、且至少有一个写、且两者之间没有通过同步建立先后顺序,则报告一次 data race。
2.4 Happens-Before 与同步
TSan 在插桩时识别这些同步点(mutex、atomic、channel 等),并在运行时更新逻辑时钟。只有“在 happens-before 上无法确定先后顺序”的两个访问,且至少一个为写时,才会被判定为 data race。
2.5 运行时行为
因此,必须使用带 race 的二进制、并且执行到存在数据竞争的代码路径,才能在日志或 stderr 中看到 “WARNING: DATA RACE” 等输出。
3. Go 中的用法
3.1 启用方式
go build -race -o myapp .
go run -race .
gotest -race ./...
生成的二进制会包含 race detector 的插桩与运行时,运行时会进行检测。
3.2 本项目中启用 Go Race
本项目通过环境变量在安装/构建脚本中可选开启:
脚本会将
-race追加到GO_BUILD_EXTRA_FLAGS,因此构建出的 agent/master 等 Go 二进制会带 race detector。3.3 如何确认当前进程是否开启 Race
go_race_detector: true表示当前进程是带-race构建的,race 检测已开启。- 若没有该行或为
false,则当前二进制未开启 race,即使代码中存在数据竞争也不会输出 “WARNING: DATA RACE”。
二进制:可用以下命令查看构建标签:
go version -m ./bin/worker若输出中包含
build -race=true,说明该二进制启用了 race。
3.4 报告格式
当检测到数据竞争时,Go 会在 stderr 打印多段 “WARNING: DATA RACE” 报告,通常包括:
- 第一次访问:位置、读/写、goroutine、调用栈;
- 第二次访问:同上;
- 相关 goroutine 的创建栈。
需要根据这些信息定位共享变量与缺失的同步(如加锁或使用 channel/atomic)。
4. C++ 中的用法(ThreadSanitizer)
使用 Clang 或 GCC 在编译和链接时加上 ThreadSanitizer:
# Clangclang++ -fsanitize=thread -g -O1 -o myapp main.cpp# 链接时也需 -fsanitize=thread# GCC
g++ -fsanitize=thread -g -O1 -o myapp main.cpp
4.2 与本项目 C++ 构建的关系
本项目的 C++ 扩展通过 scripts/BUILD.gn 和 tgn gen/build 构建,当前脚本中 enable_sanitizer=false,即默认不开启 sanitizer。若将来需要为 C++ 部分启用 TSan,需要:
- 在构建配置中打开 ThreadSanitizer(例如设置
enable_sanitizer=true并指定 thread); - 确保依赖库也以 TSan 方式编译链接,否则可能出现未定义行为或误报。
C++ TSan 的详细选项和限制请参考 LLVM/Clang 或 GCC 官方文档。
5. 限制与注意事项
- Go:带
-race 的二进制运行更慢、占用更多内存(通常数倍),仅适合在测试、开发或 CI 中使用,不适合生产环境。
- C++:同样有显著性能与内存开销,且可能增加二进制体积。
5.2 漏报与误报
5.3 使用建议
- 在单元测试、集成测试中定期使用
go test -race 或 ENABLE_RACE=1 构建并跑关键场景。
- 若日志中没有
go_race_detector: true,说明当前运行的不是 race 构建,即使存在数据竞争也不会在日志中看到 DATA RACE 报告。
- 发现 “WARNING: DATA RACE” 后,应通过加锁、使用 channel 或 atomic、或重新设计共享访问方式消除竞争,而不是在生产构建中依赖 detector“兜底”。
6. 参考资料
-race 的二进制运行更慢、占用更多内存(通常数倍),仅适合在测试、开发或 CI 中使用,不适合生产环境。5.3 使用建议
- 在单元测试、集成测试中定期使用
go test -race或ENABLE_RACE=1构建并跑关键场景。 - 若日志中没有
go_race_detector: true,说明当前运行的不是 race 构建,即使存在数据竞争也不会在日志中看到 DATA RACE 报告。 - 发现 “WARNING: DATA RACE” 后,应通过加锁、使用 channel 或 atomic、或重新设计共享访问方式消除竞争,而不是在生产构建中依赖 detector“兜底”。
No comments yet.