宏的意义
-
一些编译器宏的使用方法(clang 的线程安全注解 —— Thread Safety Annotation):
修饰类的宏
//CAPABILITY 表明某个类对象可以当作 capability 使用,其中 x 的类型是 string,能够在错误信息当中指出对应的 capability 的名称 #define CAPABILITY(x) \ THREAD_ANNOTATION_ATTRIBUTE__(capability(x)) //SCOPED_CAPABILITY 用于修饰基于 RAII 实现的 capability。 #define SCOPED_CAPABILITY \ THREAD_ANNOTATION_ATTRIBUTE__(scoped_lockable)
注:capability 是 TSA 中的一个概念,用来为资源的访问提供相应的保护。这里的资源可以是数据成员,也可以是访问某些潜在资源的函数/方法。capability 通常表现为一个带有能够获得或释放某些资源的方法的对象,最常见的就是 mutex 互斥锁。换言之,一个 mutex 对象就是一个 capability
修饰数据成员的宏:
//*GUARD_BY 用于修饰对象,表明该对象需要受到 capability 的保护 #define GUARDED_BY(x) \ THREAD_ANNOTATION_ATTRIBUTE__(guarded_by(x)) //PT_GUARDED_BY(mutex) 用于修饰指针类型变量,在更改指针变量所指向的内容前需要加锁,否则发出警告 #define PT_GUARDED_BY(x) \ THREAD_ANNOTATION_ATTRIBUTE__(pt_guarded_by(x)) //示例用法 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. }
修饰 capability 的宏
//ACQUIRED_BEFORE 和 ACQUIRED_AFTER 主要用于修饰 capability 的获取顺序,用于避免死锁 #define ACQUIRED_BEFORE(...) \ THREAD_ANNOTATION_ATTRIBUTE__(acquired_before(__VA_ARGS__)) #define ACQUIRED_AFTER(...) \ THREAD_ANNOTATION_ATTRIBUTE__(acquired_after(__VA_ARGS__)) //示例用法 Mutex m1; Mutex m2 ACQUIRED_AFTER(m1); // Alternative declaration // Mutex m2; // Mutex m1 ACQUIRED_BEFORE(m2); void foo() { m2.Lock(); m1.Lock(); // Warning! m2 must be acquired after m1. m1.Unlock(); m2.Unlock(); }
修饰函数/方法(成员函数)的宏:
//REQUIRES 声明调用线程必须拥有对指定的 capability 具有独占访问权。可以指定多个 capabilities。函数/方法在访问资源时,必须先上锁,再调用函数,然后再解锁(注意,不是在函数内解锁) #define REQUIRES(...) \ THREAD_ANNOTATION_ATTRIBUTE__(requires_capability(__VA_ARGS__)) // REQUIRES_SHARED 功能与 REQUIRES 相同,但是可以共享访问 #define REQUIRES_SHARED(...) \ THREAD_ANNOTATION_ATTRIBUTE__(requires_shared_capability(__VA_ARGS__)) //示范用法 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 表示一个函数/方法需要持有一个 capability,但并不释放这个 capability。调用者在调用被 ACQUIRE 修饰的函数/方法时,要确保没有持有任何 capability,同时在函数/方法结束时会持有一个 capability(加锁的过程发生在函数体内) #define ACQUIRE(...) \ THREAD_ANNOTATION_ATTRIBUTE__(acquire_capability(__VA_ARGS__)) //ACQUIRE_SHARED 与 ACQUIRE 的功能是类似的,但持有的是共享的 capability #define ACQUIRE_SHARED(...) \ THREAD_ANNOTATION_ATTRIBUTE__(acquire_shared_capability(__VA_ARGS__)) //示范用法 MutexLock mu; class MyClass{ public: void doSomething(){ cout << "x = " << x << endl; } MyClass() = default; void init(const int a){ x = a; } void cleanup(){ x = 0; } private: int x; }; MyClass myObject GUARDED_BY(mu); void lockAndInit(MyClass& myObject) ACQUIRE(mu) { mu.lock(); myObject.init(10); } void cleanupAndUnlock(MyClass& myObject) RELEASE(mu) { myObject.cleanup(); } // Warning! Need to unlock mu. void test() { MyClass myObject; //局部对象掩盖了全局的 myObject 对象,而全局的 myObject 对象受到了 mu 的保护 lockAndInit(myObject); myObject.doSomething(); cleanupAndUnlock(myObject); myObject.doSomething(); } int main(void){ test(); myObject.doSomething(); // Warning! MyObject is guarded by mu return 0; } //ACQUIRE 和 ACQUIRE_SHARED 的尝试版本,第一个参数是 bool,true 代表成功,false 代表失败 #define TRY_ACQUIRE(...) \ THREAD_ANNOTATION_ATTRIBUTE__(try_acquire_capability(__VA_ARGS__)) #define TRY_ACQUIRE_SHARED(...) \ THREAD_ANNOTATION_ATTRIBUTE__(try_acquire_shared_capability(__VA_ARGS__))
//RELEASE 和 RELEASE_SHARED 与 ACQUIRE 和 ACQUIRE_SHARED 正相反,它们表示调用方在调用该函数/方法时需要先持有锁,而当函数执行结束后会释放锁(释放锁的行为发生在函数体内) #define RELEASE(...) \ THREAD_ANNOTATION_ATTRIBUTE__(release_capability(__VA_ARGS__)) #define RELEASE_SHARED(...) \ THREAD_ANNOTATION_ATTRIBUTE__(release_shared_capability(__VA_ARGS__)) //实例用法 template <class T> class CAPABILITY("mutex") Container { private: Mutex mu; T* data; public: // Hide mu from public interface. void Lock() ACQUIRE() { mu.Lock(); } void Unlock() RELEASE() { mu.Unlock(); } T& getElem(int i) { return data[i]; } }; void test() { Container<int> c; c.Lock(); int i = c.getElem(0); c.Unlock(); }
//EXCLUDES 用于显式声明函数/方法不应该持有某个特定的 capability。由于 mutex 的实现通常是不可重入的,因此 EXCLUDES 通常被用来预防死锁 #define EXCLUDES(...) \ THREAD_ANNOTATION_ATTRIBUTE__(locks_excluded(__VA_ARGS__)) //实例用法 Mutex mu; int a GUARDED_BY(mu); void clear() EXCLUDES(mu) { mu.Lock(); a = 0; mu.Unlock(); } void reset() { mu.Lock(); clear(); // Warning! Caller cannot hold 'mu'. mu.Unlock(); }
//ASSERT_* 表示在运行时检测调用线程是否持有 capability #define ASSERT_CAPABILITY(x) \ THREAD_ANNOTATION_ATTRIBUTE__(assert_capability(x)) #define ASSERT_SHARED_CAPABILITY(x) \ THREAD_ANNOTATION_ATTRIBUTE__(assert_shared_capability(x))
//NO_THREAD_SAFETY_ANALYSIS 表示关闭某个函数/方法的 TSA 检测,通常只用于两种情况:1,该函数/方法可以被做成非线程安全;2、函数/方法太过复杂,TSA 无法进行检测 #define NO_THREAD_SAFETY_ANALYSIS \ THREAD_ANNOTATION_ATTRIBUTE__(no_thread_safety_analysis)__ //示范用法(NO_THREAD_SAFETY_ANALYSIS 并不是函数接口的一部分,故相比于放在头文件中,放在 cc 文件中更为恰当) class Counter { Mutex mu; int a GUARDED_BY(mu); void unsafeIncrement() NO_THREAD_SAFETY_ANALYSIS { a++; } }; //RETURN_CAPABILITY 通常用于修饰那些被当作 capability getter 的函数,这些函数会返回 capability 的引用或指针 #define RETURN_CAPABILITY(x) \ THREAD_ANNOTATION_ATTRIBUTE__(lock_returned(x))
#define MCHECK(ret) ({ __typeof__ (ret) errnum = (ret); \ assert(errnum == 0); (void) errnum;}) class CAPABILITY("mutex") MutexLock : noncopyable{ public: MutexLock() : holder_(0) { MCHECK(pthread_mutex_init(&mutex_, NULL)); } ... };
注:
- clang 的用法和 g++ 比较类似,要测试上面代码可以使用命令:clang -c -Wthread-safety example.cpp
clang 的编译选项
- -Wthread-safety 打开线程安全注解
- -Wthread-safety-negative
Negative Capability
线程安全注解(TSA)的引入旨在预防竞态条件和死锁的发生。GUARDED_BY 和 REQUIRES 通过调用者确保在读/写数据之前取得互斥锁,从而避免竞态的发生,而 EXCLUDES 则通过保证某个调用者不持有锁来避免死锁的发生。
但是,EXCLUDES 通常只是可选选项,它并不能保证得到和 REQUIRES 同样级别的安全性,特别是在以下两种情况下:
- 一个函数可以持有其他非 exclude 的 capability
- 一个 exclude 的函数 f1 调用了另一个非 exclude 的函数 f2,而 f2 持有 f1 所 exclude 的 capability。换句话讲,exclude 属性不能在多个函数之间传递
针对第二种情况,有一个例子:
class Foo {
Mutex mu;
void foo() {
mu.Lock();
bar(); // No warning.
baz(); // No warning.
mu.Unlock();
}
void bar() { // No warning. (Should have EXCLUDES(mu)).
mu.Lock();
// ...
mu.Unlock();
}
void baz() {
bif(); // No warning. (Should have EXCLUDES(mu)).
}
void bif() EXCLUDES(mu);
};
通过 REQUIRES( !mu) 来代替 EXCLUDES 则可以避免这种情况:
class FooNeg {
Mutex mu;
void foo() REQUIRES(!mu) { // foo() now requires !mu.
mu.Lock();
bar();
baz();
mu.Unlock();
}
void bar() {
mu.Lock(); // WARNING! Missing REQUIRES(!mu).
// ...
mu.Unlock();
}
void baz() {
bif(); // WARNING! Missing REQUIRES(!mu).
}
void bif() REQUIRES(!mu);
};
Negative Capability 通常是默认关闭的,因为它会导致已有的代码产生许多的警告信息。可以通过选项 -Wthread-safety-negative 来打开
使用 TSA 的一些注意事项
一般而言,注解通常被当作函数接口的一部分进行解析,因此最好放在头文件当中,而不是 .cc 文件当中。(NO_THREAD_SAFETY_ANALYSIS 除外)
-
TSA 的解析与检测主要在编译期间执行,因此不能对运行时才能确定的条件语句进行检测。例如以下做法是错误的:
bool b = needsToLock(); if (b) mu.Lock(); ... // Warning! Mutex 'mu' is not held on every path through here. if (b) mu.Unlock(); }
-
TSA 仅依赖于函数的属性的声明,它并不会将函数调用展开并内联到指定位置,因此下面的做法也是错误的(它使用 mu.lock() 进行显式的上锁,却希望使用函数调用来进行解锁):
template<class T> class AutoCleanup { T* object; void (T::*mp)(); public: AutoCleanup(T* obj, void (T::*imp)()) : object(obj), mp(imp) { } ~AutoCleanup() { (object->*mp)(); } }; Mutex mu; void foo() { mu.Lock(); AutoCleanup<Mutex>(&mu, &Mutex::Unlock); // ... } // Warning, mu is not unlocked.
-
TSA 无法追踪指针的指向,因此当两个指针指向一个互斥锁时,会导致警告的发生,如下:
class MutexUnlocker { Mutex* mu; public: MutexUnlocker(Mutex* m) RELEASE(m) : mu(m) { mu->Unlock(); } ~MutexUnlocker() ACQUIRE(mu) { mu->Lock(); } }; Mutex mutex; void test() REQUIRES(mutex) { { MutexUnlocker munl(&mutex); // unlocks mutex doSomeIO(); } // Warning: locks munl.mu }
mun1 中的成员变量 mu 在析构的时候被释放,但 TSA 并不能意识到 mutex 与 mun1.mu 指向了同一个互斥锁。因此,会显示出警告信息:mun1.mu 未上锁。