clang3.5线程安全分析
名词
Program point:程序点。程序执行的位置。
False negative:假阴 看起来没问题,其实有问题。
False positive:假阳 看起来有问题,其实没问题。
Inter-procedural:过程间的
Intra-procedural:过程内的
简介
Clang线程安全分析是c++的一个语言扩展功能,用来对代码中的竞态条件作出警告。分析完全是静态的(只消耗编译时间),没有运行时开销。这项功能由Google开发,虽然目前还在开发中,但已经足够成熟,完全可以在生产环境下使用,Google内部就广泛地使用着。
在多线程程序中,线程安全分析就像类型系统一样。除了声明数据的类型(比如说int, float)外,码农还可以声明如何控制数据的访问。打个例子,如果foo由互斥量mu守卫,在没有锁定mu的情况下读写foo,线程安全分析会报一个警告。同样地,如果存在一些特定的例程(routines),这些例程只能由GUI线程调用,而其他线程调用了这些例程,那线程安全分析也会报警告。
开始使用
#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) EXCLUSIVE_LOCKS_REQUIRED(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,由此保证增减操作是原子性的。类似地,EXCLUSIVE_LOCKS_REQUIRED属性声明:线程必须先锁定mu,才能调用withdrawImpl。因为调用者锁定了mu,在方法(method)内就可以安全地修改balance。
depositImpl()方法没有EXCLUSIVE_LOCKS_REQUIRED属性,安全分析会报一个警告。因为安全分析不是过程间分析的(inter-procedural),所以调用要求必须显示声明。(意思是编译器不会联系上下文去分析这个方法需要加上什么属性,也不会自动帮你加上,所以需要显示地手动加上)transferFrom()里也会报一个警告,因为尽管锁定了this->mu,但并没有锁定b.mu,安全分析知道这是两个不同的互斥量。
withdraw()方法里也有一个警告,因为它没有解锁mu。每个锁定都必须有一个对应的解锁,安全分析会分析每对加锁/解锁。一个函数允许获取一个锁而不用释放这个锁(反过来也一样),但必须要标上属性,用LOCK/UNLOCK_FUNCTION。
运行分析
使用-Wthread-safety标志
clang -c -Wthread-safety example.cpp
基本概念:能力(Capabilities)
线程安全分析使用能力,提供了一种保护资源的方法。这里所说的资源可以是数据成员,或者一个访问底层资源的方法/函数。安全分析确保了调用线程不能读写数据,不能调用函数,除非线程有能力。
能力与C++命名对象关联,命名对象声明了特定的方法来获取和释放能力。对象的名字用来识别这种能力。最常见的例子就是互斥量mutex。如果mu是互斥量,数据由mu保护,调用mu.Lock()会使得线程获取访问数据的能力。同样地,调用mu.Unlock()则释放这种访问数据的能力。
一个线程可能持有排它或共享这两种能力之一。排它能力一次只能由一个线程持有,而共享能力则可以由多个线程在同一时间持有。这是一种多读单写机制。读取受保护的数据只需要共享能力,写则要求排它。
在程序执行的过程中,线程持有一连串能力(比如锁定的所有互斥量),这些能力就像一串钥匙,线程拿着这些钥匙打开访问资源的大门。线程不能拷贝这些能力,也不能销毁它们,而只能从另一个线程那里获取这些能力,释放这些能力给另一个线程。标注并不知道这套获释能力的机制具体是怎么实现的,它假定了底层实现(互斥量的实现)以一种合适的方式转移能力。(意思是,标注不管实现,具体锁是怎么转移的,由Mutex实现,标注只管用就行了)
在程序运行过程中,线程持有的一连串能力是一个运行时概念。静态的安全分析计算出这些能力的一个大概值,叫做能力环境。在每个程序点,都会计算一下能力环境,并且描述那些静态持有或者不持有的能力。这个能力环境就构成了线程运行时的最小能力簇。(意思大概是,安全分析只能静态分析线程持有哪些mutex,分析不了动态持有的,所有这些静态分析得到的mutex就是线程运行时最少要获取的)
参考指南
线程安全分析使用属性来声明线程约束。属性必须附于命名的声明语句,例如类class、方法method和数据成员data member。强烈建议码农使用宏定义不同的属性,宏定义的例子能在mutex.h找到。下文都假定用宏定义。
GUARDED_BY(c) 和 PT_GUARDED_BY(c)
GUARDED_BY属性用在数据成员上,声明该数据成员受给定的能力保护。读该数据需要共享访问,写操作需要排它访问。
PT_GUARDED_BY属性作用类似,只不过是用在普通指针和智能指针。指针本身没有约束,指针指向的数据受能力保护。
Mutex mu;
int *p1 GUARDED_BY(mu); // p1受mu保护
int *p2