Clang线程安全分析 Thread Safety Analysis
1 背景
Clang线程安全分析 Thread Safety Analysis (TSA) 是 C++ 语言扩展,集成在 Clang 中的编译期静态线程安全分析工具,用于警告代码中潜在的竞争条件。 其核心思想是:通过在代码中添加属性注解(如 GUARDED_BY、REQUIRES),让编译器在编译阶段检测潜在的竞态条件(race condition),无需运行程序,零运行时开销。
将“锁”抽象为一种 能力 Capability 。
- 资源保护:资源包括数据(变量)或函数可以被某种 Capability 保护。 TSA保障调用线程必须拥有执行这个操作的“能力”,否则不能访问资源。
- 状态追踪:分析器在遍历代码时,维护一个“当前持有的 Capability 集合”。
- 合法性检查:当访问受保护资源时,检查该资源所需的 Capability 是否在当前集合中。
2 例子
#include "mutex.h" // 需要自己实现
class BankAccount {
private:
Mutex mu;
int balance GUARDED_BY(mu);
void depositImpl(int amount) {
balance += amount; // WARNING! Cannot write balance without locking mu.
}
void withdrawImpl(int amount) REQUIRES(mu) {
balance -= amount; // OK. Caller must have locked mu.
}
public:
void withdraw(int amount) {
mu.Lock();
withdrawImpl(amount); // OK. We've locked mu.
} // WARNING! Failed to unlock mu.
void transferFrom(BankAccount& b, int amount) {
mu.Lock();
b.withdrawImpl(amount); // WARNING! Calling withdrawImpl() requires locking b.mu.
depositImpl(amount); // OK. depositImpl() has no requirements.
mu.Unlock();
}
};
GUARDED_BY属性声明线程必须锁定mu才能读取或写入balance, 从而确保增量和减量操作是原子的。REQUIRES声明调用线程必须锁定mu才能调用withdrawImpl。因为调用者被假定为已锁定mu,所以修改方法主体内的balance是安全的。withdraw()方法中存在警告,因为它未能解锁mu。
2.1 运行分析
只需使用 -Wthread-safety 标志编译,例如
clang -c -Wthread-safety example.cpp
3 能力说明
TSA使用属性来声明线程约束。属性必须附加到命名声明,例如类、方法和数据成员。通常会为各种属性定义宏 示例代码
#ifndef THREAD_SAFETY_ANALYSIS_MUTEX_H
#define THREAD_SAFETY_ANALYSIS_MUTEX_H
// Enable thread safety attributes only with clang.
// The attributes can be safely erased when compiling with other compilers.
#if defined(__clang__) && (!defined(SWIG))
#define THREAD_ANNOTATION_ATTRIBUTE__(x) __attribute__((x))
#else
#define THREAD_ANNOTATION_ATTRIBUTE__(x) // no-op
#endif
#define CAPABILITY(x) \
THREAD_ANNOTATION_ATTRIBUTE__(capability(x))
#define SCOPED_CAPABILITY \
THREAD_ANNOTATION_ATTRIBUTE__(scoped_lockable)
#define GUARDED_BY(x) \
THREAD_ANNOTATION_ATTRIBUTE__(guarded_by(x))
#define PT_GUARDED_BY(x) \
THREAD_ANNOTATION_ATTRIBUTE__(pt_guarded_by(x))
#define ACQUIRED_BEFORE(...) \
THREAD_ANNOTATION_ATTRIBUTE__(acquired_before(__VA_ARGS__))
#define ACQUIRED_AFTER(...) \
THREAD_ANNOTATION_ATTRIBUTE__(acquired_after(__VA_ARGS__))
#define REQUIRES(...) \
THREAD_ANNOTATION_ATTRIBUTE__(requires_capability(__VA_ARGS__))
#define REQUIRES_SHARED(...) \
THREAD_ANNOTATION_ATTRIBUTE__(requires_shared_capability(__VA_ARGS__))
#define ACQUIRE(...) \
THREAD_ANNOTATION_ATTRIBUTE__(acquire_capability(__VA_ARGS__))
#define ACQUIRE_SHARED(...) \
THREAD_ANNOTATION_ATTRIBUTE__(acquire_shared_capability(__VA_ARGS__))
#define RELEASE(...) \
THREAD_ANNOTATION_ATTRIBUTE__(release_capability(__VA_ARGS__))
#define RELEASE_SHARED(...) \
THREAD_ANNOTATION_ATTRIBUTE__(release_shared_capability(__VA_ARGS__))
#define RELEASE_GENERIC(...) \
THREAD_ANNOTATION_ATTRIBUTE__(release_generic_capability(__VA_ARGS__))
#define TRY_ACQUIRE(...) \
THREAD_ANNOTATION_ATTRIBUTE__(try_acquire_capability(__VA_ARGS__))
#define TRY_ACQUIRE_SHARED(...) \
THREAD_ANNOTATION_ATTRIBUTE__(try_acquire_shared_capability(__VA_ARGS__))
#define EXCLUDES(...) \
THREAD_ANNOTATION_ATTRIBUTE__(locks_excluded(__VA_ARGS__))
#define ASSERT_CAPABILITY(x) \
THREAD_ANNOTATION_ATTRIBUTE__(assert_capability(x))
#define ASSERT_SHARED_CAPABILITY(x) \
THREAD_ANNOTATION_ATTRIBUTE__(assert_shared_capability(x))
#define RETURN_CAPABILITY(x) \
THREAD_ANNOTATION_ATTRIBUTE__(lock_returned(x))
#define NO_THREAD_SAFETY_ANALYSIS \
THREAD_ANNOTATION_ATTRIBUTE__(no_thread_safety_analysis)
GUARDED_BY(c) 和 PT_GUARDED_BY(c)
GUARDED_BY是数据成员上的属性,它声明该数据成员由给能力保护。对数据的读取操作需要共享访问权限,而写操作需要独占访问权限。PT_GUARDED_BY类似,但用于指针和智能指针。对数据成员本身没有约束,但它指向的数据受给定功能保护。
#include <memory> // 解决 unique_ptr 错误
#include "mutex.h" // 解决 PT_GUARDED_BY 错误
using namespace std;
Mutex mu;
int counter GUARDED_BY(mu) = 0; // 声明 counter 受 mu 保护
int *p1 GUARDED_BY(mu); // 声明int指针本身 受 mu 保护
int *p2 PT_GUARDED_BY(mu); // 声明int指针指向数据 受 mu 保护
unique_ptr<int> p3 PT_GUARDED_BY(mu); // 声明智能指针指向数据 受 mu 保护
void test() {
p1 = nullptr; // ❌ Warning: 这里的 p1 指针本身受保护
*p2 = 42; // ❌ Warning: p2 指向的内容受保护
p2 = new int; // ✅ OK: p2 指针本身不受保护
*p3 = 42; // ❌ Warning: p3 指向的内容受保护
p3.reset(new int); // ✅ OK: p3 变量本身不受保护
}
int main(){
test();
}
编译提示错误
src/a.cpp:12:5: warning: writing variable 'p1' requires holding mutex 'mu' exclusively [-Wthread-safety-analysis]
p1 = nullptr; // ❌ Warning: 这里的 p1 指针本身受保护
^
src/a.cpp:13:6: warning: writing the value pointed to by 'p2' requires holding mutex 'mu' exclusively [-Wthread-safety-analysis]
*p2 = 42; // ❌ Warning: p2 指向的内容受保护
^
src/a.cpp:15:6: warning: reading the value pointed to by 'p3' requires holding mutex 'mu' [-Wthread-safety-analysis]
*p3 = 42; // ❌ Warning: p3 指向的内容受保护
^
3 warnings generated.
REQUIRES(…), REQUIRES_SHARED(…)
REQUIRES是函数或方法上的属性,它声明调用线程必须对给定能力具有独占访问权限。可以指定多个能力。必须在进入函数时持有能力,并且在退出时必须仍然持有。REQUIRES_SHARED类似,但仅需要共享访问权限。
Mutex mu1, mu2;
int a GUARDED_BY(mu1);
int b GUARDED_BY(mu2);
void foo() REQUIRES(mu1, mu2) {
a = 0;
b = 0;
}
void test() {
mu1.Lock();
foo(); // Warning! Requires mu2.
mu1.Unlock();
}
这里调用foo函数前仅持有mu1锁,所以TSA会发出警告
ACQUIRE(…), ACQUIRE_SHARED(…), RELEASE(…), RELEASE_SHARED(…), RELEASE_GENERIC(…)
ACQUIRE和ACQUIRE_SHARED是函数或方法上的属性,声明该函数获取能力,但不释放它。进入时必须不持有给定能力,退出时将持有(对ACQUIRE为独占,对ACQUIRE_SHARED为共享)。RELEASE、RELEASE_SHARED和RELEASE_GENERIC声明该函数释放给定能力。必须在进入时持有该能力(对RELEASE为独占,对RELEASE_SHARED为共享,对RELEASE_GENERIC为独占或共享),并且在退出时将不再持有。
Mutex mu;
MyClass myObject GUARDED_BY(mu);
void lockAndInit() ACQUIRE(mu) {
mu.Lock();
myObject.init();
}
void cleanupAndUnlock() RELEASE(mu) {
myObject.cleanup();
} // Warning! Need to unlock mu.
void test() {
lockAndInit();
myObject.doSomething();
cleanupAndUnlock();
myObject.doSomething(); // Warning, mu is not locked.
}
4 源码分析
TSA在llvm项目中核心代码位置
| 核心文件 | 功能描述 |
|---|---|
clang/include/clang/Analysis/Analyses/ThreadSafety.h |
外部调用接口,定义了 runThreadSafetyAnalysis 函数。 |
clang/lib/Analysis/ThreadSafety.cpp |
核心实现文件。包含 CFG 遍历、状态合并和核心检查逻辑。 |
clang/lib/Analysis/ThreadSafetyLogical.h |
处理复杂的逻辑表达式(如 OR 关系的锁检查)。 |
clang/lib/Analysis/ThreadSafetyCommon.cpp |
提供一些通用的辅助类,如 SExpr(系统表达式)的转换。 |
clang/lib/Analysis/ThreadSafetyTIL.cpp |
TIL 类型系统,简化AST/CFG表示。 |
clang/include/clang/Basic/Attr.td |
定义了 GUARDED_BY、REQUIRES 等特性的声明,由 TableGen 生成具体的 C++ 属性类。 |
核心实现 ThreadSafety.cpp
代码文件 /llvm-project/cliang/lib/Analysis/ThreadSafety.cpp
主分析流程(第 1058 行)
/// Check a function's CFG for thread-safety violations.
///
/// We traverse the blocks in the CFG, compute the set of mutexes that are held
/// at the end of each block, and issue warnings for thread safety violations.
/// Each block in the CFG is traversed exactly once.
void runAnalysis(AnalysisDeclContext &AC);

- 初始化阶段(第 2235-2274 行)
- 创建 CFGWalker 遍历 CFG
- 跳过带有 NO_THREAD_SAFETY_ANALYSIS 属性的函数
- 跳过构造函数和析构函数(因为 this 有唯一访问权)
- 为所有 CFG 块分配 BlockInfo
- 计算局部变量的 SSA 形式
- 入口锁集设置(第 2279-2336 行)
- 处理函数属性:
- requires_capability: 调用者必须持有的锁
- release_capability: 函数释放的锁
- acquire_capability: 函数获取的锁
- 将这些锁加入入口锁集
- 处理函数属性:
- CFG 遍历-进行数据流分析(第 2338-2444 行)
- 按后序遍历所有基本块
- 收集所有前驱块的出口锁集
- 计算交集作为当前块的入口锁集
- 使用 BuildLockset 访问块内所有语句,更新锁集
- 处理后向边(循环),检查循环迭代间的锁一致性
- 按后序遍历所有基本块
- 退出检查(第 2446-2473 行)
- 比较入口和出口的锁集
- 检查是否有锁在函数结束时仍未释放
- 根据 acquire/release 属性调整期望的出口锁集
ThreadSafetyAnalyzer 是一个基于静态分析的数据流分析器,它:
- 使用事实集(FactSet)(包括入口锁集和出口锁集)跟踪程序执行过程中持有的锁
- 采用控制流图(CFG)遍历算法
- 实现了类似 SSA 的局部变量追踪机制
- 支持过程内分析(单个函数内部)
- 通过Handler 模式报告错误
例子
void lock() __attribute__((acquire_lock())); // 类型 1:加锁
void unlock() __attribute__((release_lock())); // 类型 2:解锁
void protected_func() __attribute__((requires(mu))); // 类型 3:需锁
{
lock(); // CFG 遇到这个调用 → 添加 mu 到锁集
x = 1; // 检查:持有 mu 吗?✓
protected_func(); // 检查:持有 mu 吗?✓
unlock(); // CFG 遇到这个调用 → 从锁集移除 mu
}
TIL中间表示 ThreadSafetyTIL.cpp
代码文件 /llvm-project/cliang/lib/Analysis/ThreadSafetyTIL.cpp 这个文件实现了一个称为 TIL (Thread Intermediate Language) 的中间表示系统,用于分析和验证多线程程序中的锁机制和内存访问安全性。
将AST/CFG转换为更简单的形式。将变量、运算、函数调用等都抽象成TIL的SExpr表示。主要完成以下功能:
- 操作符转换
- 规范值追踪,用于简化变量引用链(消除别名和冗余Phi节点)
- 实现CFG拓扑排序,用于后续分析
- 计算支配关系
- 最终创建一个标准化的CFG表示,为线程安全分析做准备
TIL和ThreadSafety中CFG关系图

为什么要两套CFG?
- Clang CFG:忠实反映源代码结构,但难以进行语义分析
- TIL SCFG:抽象的中间表示,便于:
- 语义等价比较(判断两个锁表达式是否相同)
- SSA 重命名(处理变量赋值)
- 模式匹配(支持通配符)
补充知识点:
SSA
(Static Single Assignment,静态单赋值)) 是一种中间表示形式,它的核心思想是:
- 每个变量只被赋值一次
- 每次赋值都创建一个新版本的变量
入口锁集和出口锁集
入口锁集(Entry Lockset)和出口锁集(Exit Lockset)
struct CFGBlockInfo {
// Lockset held at entry to block
FactSet EntrySet; // ← 入口锁集
// Lockset held at exit from block
FactSet ExitSet; // ← 出口锁集
// ... 其他成员
};
每个 CFG 基本块(Basic Block)都有两个锁集
- 入口锁集:进入这个基本块时持有的锁
- 出口锁集:离开这个基本块后持有的锁
支配关系
- Dominator (支配者):如果从起点到 B 点必须经过 A 点,那么 A 就是 B 的支配者。
- Post-Dominator (后支配者):如果从 B 点到终点必须经过 C 点,那么 C 就是 B 的后支配者。