Contents

Clang线程安全分析 Thread Safety Analysis

1 背景

Clang线程安全分析 Thread Safety Analysis (TSA) 是 C++ 语言扩展,集成在 Clang 中的编译期静态线程安全分析工具,用于警告代码中潜在的竞争条件。 其核心思想是:通过在代码中添加属性注解(如 GUARDED_BYREQUIRES),让编译器在编译阶段检测潜在的竞态条件(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_BYREQUIRES 等特性的声明,由 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);

/TSA%E5%88%86%E6%9E%90%E6%B5%81%E7%A8%8B.png

  1. 初始化阶段(第 2235-2274 行)
    • 创建 CFGWalker 遍历 CFG
    • 跳过带有 NO_THREAD_SAFETY_ANALYSIS 属性的函数
    • 跳过构造函数和析构函数(因为 this 有唯一访问权)
    • 为所有 CFG 块分配 BlockInfo
    • 计算局部变量的 SSA 形式
  2. 入口锁集设置(第 2279-2336 行)
    • 处理函数属性:
      • requires_capability: 调用者必须持有的锁
      • release_capability: 函数释放的锁
      • acquire_capability: 函数获取的锁
    • 将这些锁加入入口锁集
  3. CFG 遍历-进行数据流分析(第 2338-2444 行)
    • 按后序遍历所有基本块
      • 收集所有前驱块的出口锁集
      • 计算交集作为当前块的入口锁集
      • 使用 BuildLockset 访问块内所有语句,更新锁集
      • 处理后向边(循环),检查循环迭代间的锁一致性
  4. 退出检查(第 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表示。主要完成以下功能:

  1. 操作符转换
  2. 规范值追踪,用于简化变量引用链(消除别名和冗余Phi节点)
  3. 实现CFG拓扑排序,用于后续分析
  4. 计算支配关系
  5. 最终创建一个标准化的CFG表示,为线程安全分析做准备

TIL和ThreadSafety中CFG关系图

/TIL%E5%92%8CThreadSafety.png

为什么要两套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 的后支配者。