ThreadSanitizer 사용법, C++ 데이터 레이스를 어떻게 찾나
한 줄 답: 컴파일·링크에 -fsanitize=thread를 넣고 -g와 -O1 이상으로 빌드한 뒤 실행하면, ThreadSanitizer(TSan)가 데이터 레이스를 stderr에 보고합니다.
이 글은 Clang ThreadSanitizer 문서와 GCC Instrumentation Options의 -fsanitize=thread 설명을 기준으로 한 일반 howto입니다. AddressSanitizer 입문이나 UBSan과의 병합은 다루지 않습니다.
TSan은 어떻게 빌드에 켜나?
한 줄 답: 컴파일과 링크 모두에 -fsanitize=thread를 주고, 파일·줄 번호를 위해 -g를, 합리적 성능을 위해 -O1 이상을 함께 씁니다.
Clang 문서의 사용법은 “Simply compile and link your program with -fsanitize=thread”입니다. 성능은 대략 5×–15× 느려지고, 메모리 오버헤드는 대략 5×–10×입니다. 지원 OS·아키텍처는 문서의 Supported Platforms를 따릅니다(예: Linux aarch64/x86_64 등). 32비트는 계획되지 않았습니다.
clang++ -fsanitize=thread -g -O1 -o app main.cpp
# 또는
g++ -fsanitize=thread -g -O1 -o app main.cpp
문서 예시처럼 공유 전역에 두 스레드가 동기화 없이 쓰면 레이스가 납니다.
#include <pthread.h>
int Global;
void *Thread1(void *x) {
Global = 42;
return x;
}
int main() {
pthread_t t;
pthread_create(&t, NULL, Thread1, NULL);
Global = 43;
pthread_join(t, NULL);
return Global;
}
런타임 플래그는 TSAN_OPTIONS로 넘깁니다. 예: halt_on_error=1, history_size=7, log_path=.... 전체 목록은 TSAN_OPTIONS=help=1 ./app으로 확인할 수 있습니다. 일부 함수만 계측을 줄이려면 __attribute__((no_sanitize("thread"))) 또는 ignorelist(src:/fun:)를 문서대로 사용합니다.
일반적으로 계측된 코드가 전부 -fsanitize=thread로 빌드되어야 합니다. 미계측 동적 라이브러리가 있으면 미검출·오탐이 날 수 있습니다. PIE가 아닌 실행 파일은 지원되지 않으며, 플래그가 -fPIE/-pie처럼 동작하게 만듭니다. 정적 libc/libstdc++ 링크는 지원되지 않습니다.
리포트에서 레이스 구간을 어떻게 읽나?
한 줄 답: WARNING: ThreadSanitizer: data race 아래에서 현재 접근(Write/Read of size N) 과 이전 접근(Previous write/read) 스택, 그리고 관련 스레드의 created at 를 짝지어 같은 주소의 비원자적 동시 접근을 찾습니다.
Clang 문서 예시 출력의 골격은 다음과 같습니다.
WARNING: ThreadSanitizer: data race (pid=...)
Write of size 4 at 0x... by thread T1:
#0 Thread1 tiny_race.c:4 ...
Previous write of size 4 at 0x... by main thread:
#0 main tiny_race.c:10 ...
Thread T1 (running) created at:
#0 pthread_create ...
#1 main tiny_race.c:9 ...
읽는 순서의 실무 포인트는 다음과 같습니다.
- 주소와 크기: 같은 위치·크기의 접근이 두 스레드(또는 메인과 워커)에서 겹칩니다.
- 접근 종류: plain write끼리, 또는 write와 read가 동기화 없이 겹치면 레이스로 보고됩니다. 원자와 plain 접근 사이의 레이스는
report_atomic_races(기본 true)로 제어합니다. - 스택과 생성 지점: 어느 함수·줄에서 접근했는지, T1이
pthread_create로 어디서 만들어졌는지를 따라가면 재현 경로가 잡힙니다. - 심볼:
-g가 있어야 파일명·줄 번호가 의미 있습니다. “failed to restore the stack”이 보이면history_size를 키워 볼 수 있습니다(0–7).
억제가 필요하면 TSAN_OPTIONS=suppressions=/path/to/file.supp 또는 __tsan_default_suppressions로 race: 패턴을 넣습니다. 근본 수정 전 임시 수단입니다.
ASan과 같이 쓰면 안 되는 이유는?
한 줄 답: GCC Instrumentation Options에 따르면 -fsanitize=address는 -fsanitize=thread와 결합할 수 없고, ThreadSanitizer도 AddressSanitizer와 같은 빌드에 합치지 않는 것이 공식 제약입니다. 메모리 오류와 데이터 레이스는 별도 빌드·별도 실행으로 검사합니다.
두 도구는 각각 다른 섀도 메모리·런타임·계측을 씁니다. 같은 바이너리에 동시에 올리면 지원되지 않는 조합이 됩니다. 실무에서는 ASan 빌드와 TSan 빌드를 CI 잡으로 나누고, 같은 -g 소스에 플래그만 바꿉니다.
추가로 Linux에서 ASLR을 끄면 TSan이 섀도 메모리를 mmap하지 못해 FATAL: ThreadSanitizer can not mmap the shadow memory가 날 수 있습니다. GDB는 기본적으로 ASLR을 끄므로, TSan 바이너리를 GDB로 볼 때는 set disable-randomization off가 문서에 안내되어 있습니다.
TSan 런타임은 버그 검출용이며 프로덕션 실행 파일에 링크할 목적이 아닙니다(Clang Security Considerations).
FAQ
Q. CMake에서는?
A. target_compile_options와 target_link_options(또는 CMAKE_CXX_FLAGS/CMAKE_EXE_LINKER_FLAGS)에 동일하게 -fsanitize=thread를 넣고, Debug/RelWithDebInfo처럼 -g가 있는 구성을 쓰는 편이 안전합니다.
Q. 데드락도 보나?
A. detect_deadlocks(기본 true)로 뮤텍스 관련 데드락 검출을 켤 수 있습니다. 데이터 레이스 본문과는 별 항목입니다.