# ThreadSanitizer(TSan)使用说明

ThreadSanitizer(TSan)是 Clang/GCC 中用于检测多线程程序中数据竞争(data race)的工具。它能够在程序运行时分析线程之间的共享内存访问,如果发现两个线程对同一块内存同时进行了读写,且没有正确使用互斥量、原子操作或其他同步机制,就会报告一个 race warning。

# 1. TSan 是什么

TSan 的核心目标是帮助开发者发现并定位多线程程序中的数据竞争问题。与普通的逻辑错误不同,数据竞争往往不会立刻导致崩溃,而是表现为偶发错误、结果不稳定、测试不可靠等现象。

例如:

  • 两个线程同时对同一个全局变量进行读写。
  • 一个线程在读取变量时,另一个线程正在修改它。
  • 代码中没有使用 std::mutexstd::atomic 或其他同步机制。

# 2. 基本原理

TSan 的工作方式可以概括为:

  1. 在编译时插桩
    • 编译器会在程序中加入额外的检测逻辑。
  2. 运行时跟踪线程访问
    • TSan 会记录每次内存访问发生在哪个线程、访问了哪个地址、访问类型是读还是写。
  3. 检测冲突
    • 当两个线程对同一地址发生了没有同步保护的并发访问时,就会报告 data race。

它的优点是:

  • 能在程序实际运行时发现并发问题。
  • 报错信息较详细,能指出具体的线程、调用栈和内存地址。
  • 对多线程调试非常有效。

# 3. 主要功能

TSan 能检测的典型问题包括:

  • 数据竞争(data race)
  • 未加锁的共享变量访问
  • 多线程下的竞态条件
  • 不正确使用 std::thread 时的并发访问问题

# 4. 使用依赖

TSan 通常依赖以下条件:

  • GCC 或 Clang 编译器
  • 编译时开启 -fsanitize=thread
  • 推荐使用 -g 保留调试信息,方便定位源码行号

常见编译命令:

g++ -g -fsanitize=thread -fno-omit-frame-pointer -O1 -o tsan_demo tsan_demo.cpp

其中:

  • -fsanitize=thread:开启 TSan 检测
  • -g:保留调试信息
  • -O1:降低优化影响,便于观察问题
  • -fno-omit-frame-pointer:保留调用栈信息

# 5. 示例代码

下面是一个典型的多线程数据竞争例子:

#include <iostream>
#include <thread>

int global_counter = 0;

void increment_counter() {
  for (int i = 0; i < 100000; ++i) {
    // 这里没有加锁,两个线程会同时读写 global_counter
    global_counter++;
  }
}

int main() {
  std::thread t1(increment_counter);
  std::thread t2(increment_counter);

  t1.join();
  t2.join();

  std::cout << "Final Counter Value: " << global_counter << "\n";
  return 0;
}

# 6. 编译与运行

g++ -g -fsanitize=thread -fno-omit-frame-pointer -O1 -o tsan_demo tsan_demo.cpp
./tsan_demo

# 7. 扫描程序执行结果分析

执行后,TSan 通常会输出类似下面的报告:

WARNING: ThreadSanitizer: data race
  Read of size 4 at 0x... by thread T2:
    #0 increment_counter() tsan_demo.cpp:7

  Previous write of size 4 at 0x... by thread T1:
    #0 increment_counter() tsan_demo.cpp:7

SUMMARY: ThreadSanitizer: data race tsan_demo.cpp:7 in increment_counter()

从这段输出可以重点分析:

  • WARNING: ThreadSanitizer: data race:说明发现了线程竞争问题。
  • Read of size 4Previous write of size 4:说明有一个线程正在读取共享变量,另一个线程正在写入同一个变量。
  • #0 increment_counter() ...:指出问题发生的位置,通常就是出现共享变量访问的代码行。
  • SUMMARY:给出了最终结论和最关键的代码位置。

在上面的例子中,global_counter++ 这一行会同时被两个线程执行,无法保证原子性,因此 TSan 会判定为数据竞争。

# 8. 典型输出中的常见信息

TSan 的输出常常包含以下信息:

  • 线程编号:如 thread T1thread T2
  • 访问类型:读/写
  • 共享变量地址:帮助定位发生竞争的内存位置
  • 调用栈:说明是从哪一段代码路径触发的
  • 线程创建关系:说明是哪个线程创建了另一个线程

# 9. 遇到的常见报错

有时 TSan 运行时会出现下面的提示:

WARNING: found 1 unrecognized flag(s):
    intercept_segv
FATAL: ThreadSanitizer: unexpected memory mapping 0x...

这通常表示程序或某个库在 TSan 启动前就占用了它保留的地址空间。常见原因是:

  • 地址空间随机化(ASLR)导致的冲突
  • 某些库在启动阶段提前 mmap 了内存

可以尝试临时关闭 ASLR 后再运行:

sudo sysctl -w kernel.randomize_va_space=0
./tsan_demo
sudo sysctl -w kernel.randomize_va_space=2

如果关闭 ASLR 后工作正常,说明问题确实与地址随机化有关。

# 10. 使用建议

在实际开发中,建议:

  • 对多线程代码先用 TSan 做扫查,再进行修复。
  • 在调试阶段优先开启 TSan。
  • 对共享变量访问使用 std::mutexstd::atomic 或其他同步机制。
  • 结合日志和测试用例一起使用,能更好地复现和定位并发问题。

# 11. 小结

TSan 是排查 C/C++ 多线程并发问题的重要工具。它能够让“看似偶发”的竞态错误变成可定位的报告,帮助开发者快速判断哪些共享变量存在数据竞争。对于并发编程、线程安全和系统稳定性优化来说,TSan 非常值得掌握。