clang的線程安全分析模塊 thread safety analysis


介紹
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 &mu; }

  // 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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM