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)

当以下三个条件同时满足时,称为发生数据竞争:

  1. 至少两个线程/ goroutine 访问同一块内存;
  2. 其中至少有一个是写操作;
  3. 这些访问之间没有通过同步原语(如 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 运行时看到。

2.3 Shadow State(影子状态)

TSan 为程序中的内存维护一份“影子”元数据(shadow memory),用来记录:

    当新的访问到来时,TSan 将当前访问与影子中记录的访问做比较:若来自不同线程、且至少有一个写、且两者之间没有通过同步建立先后顺序,则报告一次 data race。

    2.4 Happens-Before 与同步

    • 同一线程内,程序顺序定义先后;
    • 同步原语在不同线程间建立先后:例如 mutex 的 unlock happens-before 后续的 lock,channel 的 send happens-before 对应的 receive。

    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. 参考资料