静态检查工具:Clang thread safety annotations
参考连接:Thread Safety Analysis¶
总览
线程安全分析的工作原理非常类似于多线程程序的类型系统。除了声明数据类型(例如int
、float
等)之外,程序员还可以(选择性地)声明如何在多线程环境中控制对数据的访问。例如,如果foo
是由互斥锁mu
保护的,那么当一段代码在没有首先锁定mu
的情况下对foo
进行读写时,分析就会发出警告。类似地,如果有一些特定的例程应该只由GUI线程调用,那么分析将在其他线程调用这些例程时发出警告。
主要宏一览
除NO_THREAD_SAFETY_ANALYSIS需放在定义,其余均放于声明处!
GUARDED_BY(mu) |
数据成员收到mu保护,读共享,写排他(即写前锁) |
---|---|
REQUIRES(mu) |
调用函数或方法前,需要mu,即函数进入前已持有,退出后仍持有 |
ACQUIRE(mu) |
调用函数或方法时,持有mu,即函数进入时才持有,退出后不释放 |
RELEASE(mu) |
调用函数或方法时,释放mu,即函数进入前已持有,退出前释放 |
EXCLUDES(mu) |
调用函数或方法时,不需mu,即函数进入前不能持有,退出后自然不能释放 |
NO_THREAD_SAFETY_ANALYSIS |
调用函数或方法时,关闭对其线程安全检查 |
RETURN_CAPABILITY(mu) |
**声明函数返回对给定能力的引用,**用于注释返回互斥对象的getter 方法。 |
CAPABILITY(<string>) |
指定**类的对象(this)**可以作为能力使用,配合无参数ACQUIRE 和RELEASE 使用 |
SCOPE_CAPABILITY |
实现RAII-style 锁的类的一个属性,其功能在构造函数中获得,在析构函数中释放 |
TRY_ACQUIRE<bool,mu> |
试图获得给定的功能,并返回一个指示成功或失败的布尔值。 |
ASSET_CAPABILITY(mu) |
它断言调用线程已经拥有给定的能力 |
- 其余关键组合词(如ACQUIRE与ACQUIRE_SHARED)
PT
pointerSHARED
支持共享访问GENERIC
支持独占和共享访问
入门实操
通过代码注解(annotations)告诉编译器哪些成员变量和成员函数受mutex保护,若忘记加锁,编译器给出警告。
目的:
他人后续维护代码时,特别容易遗漏线程安全的假设。此工具能将原作者设计意图以代码注解清楚地写出来并让编译器【自动检查】!
注意:
GUARDED_BY
表明哪个成员变量被mutex
保护clang-Wthead-safety
编译代码
代码示例
#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();
}
};
编译方式:-Wthread-safety
clang -c -Wthread-safety example.cpp
代码解析
GUARDED_BY(mu)
:对于balance
读写之前,必须对mu
上锁,保证增减操作原子性。
REQUIRES(mu)
: 表示调用函数前“【假定】保证对mu
上锁,此为前提”,故函数内操作balance
是线程安全的。
在withdraw
中报错WARNING! Failed to unlock mu
.说明REQUIRES
可检测锁的解锁操作;
在24行报错WARNING! Calling withdrawImpl() requires locking b.mu.
,说明线程分析可理解不同的锁。
基本概念:功能
线程安全分析提供了一种用保护资源的能力。资源可以是数据成员,也可以是提供对底层资源访问的函数/方法。分析确保调用线程不能访问资源(即调用函数,或读写数据),除非它有能力这样做。
能力与已命名的c++对象相关联,这些对象声明特定的方法来获取和释放能力。对象的名称用于识别功能,最常见的例子是互斥锁。例如,如果mu
是一个互斥锁,那么调用mu. lock()
会使调用线程获得访问受mu保护的数据的能力。类似地,调用mu.Unlock()
会释放该能力。
一个线程的能力可共享、可独占。 比如说多读单写模式,就包含了读能力共享,写能力独占。
程序运行的任意时刻,线程均有用一组特定的能力,可访问给定资源。线程仅能与其他线程互相释放能力或获取能力,而不能拷贝能力,亦或是摧毁。【注解】假设底层实现以适当方式切换,而不知用于获取和释放的具体机制。
参考指南
线程安全分析使用属性来声明线程约束。属性必须附加到命名声明,如类、方法和数据成员。强烈建议用户为各种属性定义宏;示例定义可以在下面的mutex.h
中找到。下面的文档假设使用宏。
这些属性只控制线程安全分析做出的假设和它发出的警告。它们不会影响生成的代码或运行时的行为。
属性示例
GUARDED_BY(c)
and PT_GUARDED_BY(c)
: 数据成员
GUARDED_BY
是数据成员上的一个属性,它声明数据成员受到给定能力的保护。对数据的读操作需要共享访问,而写操作需要排他访问。
PT_GUARDED_BY
与上述,但用于指针和智能指针。数据成员本身没有约束,但是它所指向的数据受到给定能力的保护。
GUARDED_BY(mu)
能力:
能力为mu
,为数据成员p1
提供mu
的保护,即对数据的读操作需要共享访问,而写操作需要排他访问。
Mutex mu;
int p1 GUARDED_BY(mu);
int *p2 PT_GUARDED_BY(mu);
unique_ptr<int> p3 PT_GUARDED_BY(mu);
void test() {
p1 = 0; // Warning!
*p2 = 42; // Warning!
p2 = new int; // OK.
*p3 = 42; // Warning!
p3.reset(new int); // OK.
}
REQUIRES(…)
, REQUIRES_SHARED(…)
:函数或方法
REQUIRES
是函数或方法上的一个属性,它声明调用线程必须独占访问给定的能力。可以指定多个功能。功能必须在函数进入前持有,退出时后必须持有。
REQUIRES_SHARED
类似,但仅支持共享访问
REQUIRES(mu1, mu2)
能力解释:
能力为mu1,mu2,
在函数进入前持有,退出时后必须持有
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();
}
ACQUIRE(…)
, ACQUIRE_SHARED(…)
, RELEASE(…)
,RELEASE_SHARED(…)
,RELEASE_GENERIC(…)
适用于函数或方法
有参数
ACQUIRE
和ACQUIRE_SHARED
是函数或方法上的属性,声明函数获得能力,但不释放它。该能力进入前不持有,但在退出后持有(SHARED
区别同上)。
RELEASE
则相反,声明函数释放能力,即能力在进入前持有,退出前释放
SHARED
:共享GENERIC
:支持独占与共享
ACQUIRE(mu)能力解释:
能力为mu
,在进入lockAndInit
后持有,退出后持有
Mutex mu;
MyClass myObject GUARDED_BY(