介紹
Clang的線程安全分析模塊是C++語言的一個擴展,能對代碼中潛在的競爭條件進行警告。這種分析是完全靜態的(即編譯時進行),沒有運行時的消耗。當前這個功能還在開發中,但它已經具備了足夠的成熟度,可以被部署到生產環境中。它由Google開發,同時受到CERT(United States Computer Emergency Readiness Team,美國互聯網應急中心)/SEI(Software Engineering Institute,軟件工程中心)的協助,並在Google的內部代碼中被廣泛應用。
對於多線程的程序來說,線程安全分析很像一個類型系統。在一個多線程的環境中,程序員除了可以聲明一個數據的類型(比如,int, float等)之外,還可以聲明對數據的訪問是如何被控制的。例如,如果變量foo受到互斥鎖mu的監控,那么如果如果一段代碼在讀或者寫foo之前沒有加鎖,就會發出警告。同樣,如果一段僅應被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) 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屬性聲明,一個線程在讀或寫balance變量之前,必須先鎖住mu,由此保證對balance的增加和降低操作都是原子的。同樣,REQUIRES聲明了在調用線程調用withdrawImpl方法之前,必須先鎖住mu。因為調用者已經在方法調用之前鎖住了mu,因此在方法體內部修改balance就是安全的了。
depositeImpl方法沒有REQUIRES生命,因此分析模塊給出了一個警告。線程安全分析模塊並不是進程內部的,因此對調用者的需求必須被顯式的聲明。在transferFrom方法內部也有一個警告,因為盡管方法鎖住了this->mu,它沒有鎖住b.mu,分析模塊知道這是兩個不同的鎖,分屬兩個不同的對象。
最后,在withdraw方法內部也有一個警告,因為它沒有解鎖mu。每一個上鎖操作必須有一個配對的解鎖操作,分析模塊將檢測成對的上鎖和解鎖操作。一個函數可以僅上鎖而不解鎖(反之亦然),但這必須被顯式標注(使用ACQUIRE/RELEASE)。
運行分析
為了運行分析模塊,只需要加入編譯選項 -Wthread-safety,比如
clang -c -Wthread-safety example.cpp
注意,這段代碼假設已經有一個正確的標注文件mutex.h存在,這個文件中聲明了哪個方法執行了上鎖、解鎖的操作。
基本概念:監護權
線程安全分析提供了一種使用“監護權”保護資源的方法。“資源”可以是數據成員,或者可以訪問底層資源的過程或方法。分析模塊保證了,除非調用者線程擁有了對於資源的監護權(調用一個方法,或者讀/寫一個數據),否則它是無法訪問到資源的。監護權被綁定到一些具名的C++對象上,這些對象聲明了專用的方法來獲取和釋放監護權。這些對象的名稱被用來識別監護權。最常見的例子就是互斥鎖。例如,如果mu是一個互斥鎖,那么調用mu.Lock()使得調用者線程擁有了mu所保護的數據的監護權。同樣的,調用mu.Unlock()釋放監護權。
線程可以排他的或者共享的擁有監護權。一個排他的監護權每次僅能被一個線程擁有,而一個共享的監護權可以同時被多個線程擁有。這個機制使得多讀一寫的模式成為可能。寫操作需要排他的監護權,而讀操作僅需要共享的監護權。
在程序執行的給定時刻,每個線程擁有各自的監護權集合(該線程鎖住的互斥鎖的集合)。它們類似於鑰匙或者令牌,允許線程訪問這些資源。跟物理上的安全鑰匙一樣,線程不能復制、也不能銷毀監護權。一個線程只能把監護權釋放給另外一個線程,或者從另外一個線程獲得監護權。安全起見,分析模塊的標識不清楚具體獲取和釋放監護權的機制,它假設底層實現(例如,互斥鎖的實現)能夠恰當的完成這個任務。
在程序運行的某個具體時刻,某個線程擁有的監護權集合是一個運行時的概念。靜態的任務是對這個集合(也被稱為監護權環境)進行估計。分析模塊會通過靜態分析描述程序任何執行節點的監護權環境。這個估計,是對實際運行時監護權環境的保守估計。
應用指導
線程安全分析模塊使用屬性來聲明線程約束。屬性必須被綁定到具名的聲明,比如類、方法、數據成員。我們強烈建議用戶為這些不同的屬性定義宏,示例請參見以下的mutex.h文件。接下來的說明將假設使用了宏。
由於歷史原因,線程安全分析模塊的早期版本是用了以鎖為中心的宏名稱。為了適應更普適的模型,這些宏被更改了名稱。之前的名稱仍然在使用,在接下來的文檔里會特別指明。
GUARDED_BY(c) 和 PT_GUARDED_BY(c)
GUARDED_BY是一個應用在數據成員上的屬性,它聲明了數據成員被給定的監護權保護。對於數據的讀操作需要共享的訪問權限,而寫操作需要獨占的訪問權限。
PT_GUARDED_BY與之類似,只不過它是為指針和智能指針准備的。對數據成員(指針)本身沒有任何限制,它保護的是指針指向的數據。
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(...)
早期的版本是EXCLUSIVE_LOCKS_REQUIRED,SHARED_LOCKS_REQUIRED
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(); }
ACQUIRE(...),ACQUIRE_SHARED(...),RELEASE(...),RELEASE_SHARED(...)
早期版本是EXECLUSIVE_LOCK_FUNCTION,SHARED_LOCK_FUNCTION,UNLOCK_FUNCTION
ACQUIRE是一個作用在函數或者方法上的屬性,它聲明了這個函數或方法需要一個監護權,但不會釋放它。調用者在調用之前不能擁有監護權,在調用之后需要獲得監護權。ACQUIRE_SHARED與之類似。
RELEASE和RELEASE_SHARED聲明,函數必須釋放監護權。調用者在調用之前必須擁有監護權,在調用之后將失去監護權。監護權是共享還是排他的,並不重要。
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. }
如果沒有向ACQUIRE或RELEASE傳遞參數,那么this將會成為它的默認參數,分析模塊將不會檢查它修飾的函數體。這種模式通常被在抽象接口下隱藏具體鎖細節的類使用(譯者注:為了不向外界暴露鎖的實現細節,將鎖作為類的私有數據,因此,對共有函數聲明不帶參數的ACQUIRE/RELEASE,相當於對當前對象——也相當於對這個私有的鎖——進行加鎖/釋放鎖操作),示例如下:
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(...)
早期版本LOCKS_EXCLUDED
EXCLUDES是一種函數或方法的屬性,用來聲明調用者絕對不能擁有監護權。這樣做的目的是為了防止死鎖。很多互斥鎖的實現是不允許重入的,因此如果一個函數二次申請一個互斥鎖,會引起死鎖。
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(); }
與REQUIRES不同,EXCLUDES是可選的。如果該屬性缺失的話,分析模塊不會發出警告,這在某些情況下可能會產生某些錯誤的負樣本(本來應該在函數內部進行加鎖和釋放鎖,但沒有這么做,分析系統也沒有警告,這樣在實際運行中可能會出現錯誤)。這個問題將在“負監護權”章節中討論。
NO_THREAD_SAFETY_ANALYSIS
NO_THREAD_SAFETY_ANALYSIS是一種函數或方法的屬性,它意味着對該函數關閉線程安全分析。它為以下兩種函數的實現提供了可能,第一,故意設計的線程不安全的代碼,第二,代碼是線程安全的,但是對於線程安全分析模塊來說太復雜,模塊無法理解。第二種情況將在“已知限制”章節中討論。
class Counter { Mutex mu; int a GUARDED_BY(mu); void unsafeIncrement() NO_THREAD_SAFETY_ANALYSIS { a++; } };
與其它屬性不同的是,NO_THREAD_SAFETY_ANALYSIS不是函數接口的一部分,它需要被放在源文件(cc或cpp)而不是頭文件(h)中。
RETURN_CAPABILITY(c)
早期版本LOCK_RETURNED
RETURN_CAPABILITY是一種函數或方法的屬性,它聲明了該函數將返回一個給定監護權的引用。通常用來修飾會返回互斥鎖的getter方法。
class MyClass { private: Mutex mu; int a GUARDED_BY(mu); public: Mutex* getMu() RETURN_CAPABILITY(mu) { return μ } // analysis knows that getMu() == mu void clear() REQUIRES(getMu()) { a = 0; } };
ACQUIRED_BEFORE(...),ACQUIRED_AFTER(...)
ACQUIRED_BEFORE和ACQUIRED_AFTER是成員變量的屬性,特別是用來聲明互斥鎖或其他監護權。這種聲明在互斥鎖之間強加了一個獲取的優先級,目的是為了防止死鎖。
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(); }
CAPABILITY(<string>)
早期版本LOCKABLE
CAPABILITY是一種類的屬性,它意味着該類的對象可以被當做監護權使用。string參數使用錯誤信息指定了監護權的類型,例如“mutex"。參見之前給出的”Container"示例,或者mutex.h文件中的Mutex類。
SCOPED_CAPABILITY
早期版本SCOPED_LOCKABLE
SCOPED_CAPABILITY是一種類的屬性,這種類實現了RAII風格的鎖,監護權在構造函數中獲取,在析構函數中釋放。這種類需要被特別指出,因為構造和析構函數指定的監護權的名稱是不一樣的,參見mutex.h文件中的MutexLocker類。
TRY_ACQUIRE(<bool>,...),TRY_ACQUIRE_SHARED(<bool>,...)
早期版本EXECLUSIVE_TRYLOCK_FUNCTION,SHARED_TRYLOCK_FUNCTION
這是一種函數或方法的屬性,這些函數或方法試圖獲取指定的監護權,並且返回一個布爾值表明是否成功。函數的第一個參數必須是true或者false,來說明哪個值表示監護權獲取成功,剩余參數等同於ACQUIRE。具體示例參見mutex.h。
ASSERT_CAPABILITY(...)和ASSERT_SHARED_CAPABILITY(...)
早期版本ASSERT_EXECLUSIVE_LOCK,ASSERT_SHARED_LOCK
這是一種函數或方法的屬性,它表明該函數將在運行時進行一個安全檢查,判斷調用線程是否擁有監護權。如果調用線程沒有監護權,該函數將會返回空表明調用失敗。具體示例詳見mutex.h
GUARDED_VAR和PT_GUARDED_VAR
該屬性的使用已被拋棄。
(還有部分細節,時間原因就不講了,詳見參考鏈接)
線程安全分析模塊可以被任何線程庫使用,不過它要求線程的API被包裝在有合適注釋的類或者方法里。以下的mutex.h提供了一個示例,這些函數需要被實現,以便調用合適的底層實現。
#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 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) // Defines an annotated interface for mutexes. // These methods can be implemented to use any internal mutex implementation. class CAPABILITY("mutex") Mutex { public: // Acquire/lock this mutex exclusively. Only one thread can have exclusive // access at any one time. Write operations to guarded data require an // exclusive lock. void Lock() ACQUIRE(); // Acquire/lock this mutex for read operations, which require only a shared // lock. This assumes a multiple-reader, single writer semantics. Multiple // threads may acquire the mutex simultaneously as readers, but a writer // must wait for all of them to release the mutex before it can acquire it // exclusively. void ReaderLock() ACQUIRE_SHARED(); // Release/unlock an exclusive mutex. void Unlock() RELEASE(); // Release/unlock a shared mutex. void ReaderUnlock() RELEASE_SHARED(); // Try to acquire the mutex. Returns true on success, and false on failure. bool TryLock() TRY_ACQUIRE(true); // Try to acquire the mutex for read operations. bool ReaderTryLock() TRY_ACQUIRE_SHARED(true); // Assert that this mutex is currently held by the calling thread. void AssertHeld() ASSERT_CAPABILITY(this); // Assert that is mutex is currently held for read operations. void AssertReaderHeld() ASSERT_SHARED_CAPABILITY(this); // For negative capabilities. const Mutex& operator!() const { return *this; } }; // MutexLocker is an RAII class that acquires a mutex in its constructor, and // releases it in its destructor. class SCOPED_CAPABILITY MutexLocker { private: Mutex* mut; public: MutexLocker(Mutex *mu) ACQUIRE(mu) : mut(mu) { mu->Lock(); } ~MutexLocker() RELEASE() { mut->Unlock(); } }; #ifdef USE_LOCK_STYLE_THREAD_SAFETY_ATTRIBUTES // The original version of thread safety analysis the following attribute // definitions. These use a lock-based terminology. They are still in use // by existing thread safety code, and will continue to be supported. // Deprecated. #define PT_GUARDED_VAR \ THREAD_ANNOTATION_ATTRIBUTE__(pt_guarded_var) // Deprecated. #define GUARDED_VAR \ THREAD_ANNOTATION_ATTRIBUTE__(guarded_var) // Replaced by REQUIRES #define EXCLUSIVE_LOCKS_REQUIRED(...) \ THREAD_ANNOTATION_ATTRIBUTE__(exclusive_locks_required(__VA_ARGS__)) // Replaced by REQUIRES_SHARED #define SHARED_LOCKS_REQUIRED(...) \ THREAD_ANNOTATION_ATTRIBUTE__(shared_locks_required(__VA_ARGS__)) // Replaced by CAPABILITY #define LOCKABLE \ THREAD_ANNOTATION_ATTRIBUTE__(lockable) // Replaced by SCOPED_CAPABILITY #define SCOPED_LOCKABLE \ THREAD_ANNOTATION_ATTRIBUTE__(scoped_lockable) // Replaced by ACQUIRE #define EXCLUSIVE_LOCK_FUNCTION(...) \ THREAD_ANNOTATION_ATTRIBUTE__(exclusive_lock_function(__VA_ARGS__)) // Replaced by ACQUIRE_SHARED #define SHARED_LOCK_FUNCTION(...) \ THREAD_ANNOTATION_ATTRIBUTE__(shared_lock_function(__VA_ARGS__)) // Replaced by RELEASE and RELEASE_SHARED #define UNLOCK_FUNCTION(...) \ THREAD_ANNOTATION_ATTRIBUTE__(unlock_function(__VA_ARGS__)) // Replaced by TRY_ACQUIRE #define EXCLUSIVE_TRYLOCK_FUNCTION(...) \ THREAD_ANNOTATION_ATTRIBUTE__(exclusive_trylock_function(__VA_ARGS__)) // Replaced by TRY_ACQUIRE_SHARED #define SHARED_TRYLOCK_FUNCTION(...) \ THREAD_ANNOTATION_ATTRIBUTE__(shared_trylock_function(__VA_ARGS__)) // Replaced by ASSERT_CAPABILITY #define ASSERT_EXCLUSIVE_LOCK(...) \ THREAD_ANNOTATION_ATTRIBUTE__(assert_exclusive_lock(__VA_ARGS__)) // Replaced by ASSERT_SHARED_CAPABILITY #define ASSERT_SHARED_LOCK(...) \ THREAD_ANNOTATION_ATTRIBUTE__(assert_shared_lock(__VA_ARGS__)) // Replaced by EXCLUDE_CAPABILITY. #define LOCKS_EXCLUDED(...) \ THREAD_ANNOTATION_ATTRIBUTE__(locks_excluded(__VA_ARGS__)) // Replaced by RETURN_CAPABILITY #define LOCK_RETURNED(x) \ THREAD_ANNOTATION_ATTRIBUTE__(lock_returned(x)) #endif // USE_LOCK_STYLE_THREAD_SAFETY_ATTRIBUTES #endif // THREAD_SAFETY_ANALYSIS_MUTEX_H
原文地址:https://clang.llvm.org/docs/ThreadSafetyAnalysis.html