📚

Clang のスレッドセーフ分析

2021/09/12に公開

Zircon のスピンロック実装を読んでいたときに出てきた、ClangThread Safety Analysis について紹介します。

スレッドセーフ分析とは

Clang Thread Safety Analysis は、コード内の潜在的な競合状態について警告する C ++言語拡張機能です。分析は完全に静的です(つまり、コンパイル時)。実行時のオーバーヘッドはありません。

Thread Safety Analysis — Clang 13 documentation

何をすれば、何を警告してくれるのか

  • 排他制御用のクラスを作成する

    class __attribute__((capability("mutex"))) Mutex {
    public:
       void Lock() __attribute__((acquire_capability()))   {/* 実装省略 */};
       void Unlock() __attribute__((release_capability())) {/* 実装省略 */};
    };
    
  • リソースに、排他制御用のオブジェクトを関連つける

    Mutex mu;                                          // 排他制御用のオブジェクト
    int resource  __attribute__((guarded_by(mu))) = 0; // リソース
    
    • リソース resourceに、排他制御用のオブジェクト mu を関連つける(__attribute__((guarded_by(mu)))
      resourcemu で守られる
    • resource の初期値は 0

上記を行うと、コンパイル時に下記を警告してくれます。

  • 排他制御を行わない、リソースへのアクセスを警告してくれる

    int main(void) {
      printf("%d", resource); // warning: reading variable 'resource' requires holding mutex 'mu'
      resource += 1;          // warning: writing variable 'resource' requires holding mutex 'mu' exclusively
      return 0;
    }
    
    • printf("%d", resource) で、排他制御なしのリソース resource の読み込みに警告が出る

    • resource += 1 で、排他制御なしのリソース resource の書き込みに警告が出る

    • 排他制御を行うと、警告は出ない

      int main(void) {
        mu.Lock();
        printf("%d", resource); // OK.
        resource += 1;          // OK.
        mu.Unlock();
        return 0;
      }
      
  • 二重のロック取得を警告してくれる

    int main(void) {
      mu.Lock();
      mu.Lock(); // warning: acquiring mutex 'mu' that is already held
      return 0;
    }
    
  • 二重のロック解放を警告してくれる

    int main(void) {
      mu.Lock();
      mu.Unlock();
      mu.Unlock(); // warning: releasing mutex 'mu' that was not held
      return 0;
    }
    
  • ロックの解放し忘れを警告してくれる

    int main(void) {
      mu.Lock();
      return 0;
    } // warning: mutex 'mu' is still held at the end of function
    
  • ロック未取得時の解放を警告してくれる

    int main(void) {
      mu.Unlock(); // warning: releasing mutex 'mu' that was not held
      return 0;
    }
    

その他の警告はこちら

紹介する属性

Zircon のスピンロック実装(spinlock.hspinlock.cc)に出てくる属性を中心に紹介します。

使い方

clang -Wthread-safety spinlock.cc
  • -Wthread-safety オプションをつける
使用した Clang のバージョン
$ ~/sdk/fuchsia/prebuilt/third_party/clang/linux-x64/bin/clang --version
Fuchsia clang version 13.0.0 (https://llvm.googlesource.com/a/llvm-project f5603aa050cefff9052e9085920f3aa2d1d31b86)
Target: x86_64-unknown-linux-gnu
Thread model: posix
InstalledDir: /home/junkawa/sdk/fuchsia/prebuilt/third_party/clang/linux-x64/bin

Zircon スピンロック実装での使用方法

  • 構造体にケーパビリティを関連つける

    typedef struct __attribute__((capability("mutex"))) arch_spin_lock {
      unsigned long value;
    } arch_spin_lock_t;
    
  • ケーパビリティの扱いを、関数に指定する

    • ロックを取得するまでループする関数

      void arch_spin_lock(arch_spin_lock_t *lock)
          __attribute__((acquire_capability(lock)));
      
      • arch_spin_lock() 関数は、ケーパビリティ lock を取得する(__attribute__((acquire_capability(lock)))
        • 関数に入るとき、スレッドがケーパビリティ lock を保持していないことをチェック
        • 関数から出るとき、スレッドがケーパビリティ lock を保持していることをチェック
    • 一度だけロック取得を試みる関数

      bool arch_spin_trylock(arch_spin_lock_t *lock)
          __attribute__((try_acquire_capability(false, lock)));
      
      • arch_spin_trylock() 関数は、ケーパビリティ lock を取得を試みる(__attribute__((try_acquire_capability(false, lock)))
        • 戻り値で、取得に成功したか、失敗したかを示す
        • 取得に成功した場合、第一引数の false を返す。失敗した場合 true を返す
    • 取得したロックを解放する関数

      void arch_spin_unlock(arch_spin_lock_t *lock)
          __attribute__((release_capability(lock)));
      
      • arch_spin_unlock() 関数は、ケーパビリティ lock を解放する(__attribute__((release_capability(lock)))
        • 関数に入るとき、スレッドがケーパビリティ lock を保持していることをチェック
        • 関数から出るとき、スレッドがケーパビリティlock を保持していないことをチェック
  • 関数内でのスレッドセーフ分析を無効化する

    void arch_spin_lock(arch_spin_lock_t *lock)
        __attribute__((no_thread_safety_analysis)) {/* 実装省略 */}
    
    bool arch_spin_trylock(arch_spin_lock_t *lock)
        __attribute__((no_thread_safety_analysis)) {/* 実装省略 */}
    
    void arch_spin_unlock(arch_spin_lock_t *lock)
        __attribute__((no_thread_safety_analysis)) {/* 実装省略 */}
    
    • 関数の内部で、ロックの取得、解放が行われたかどうかは分析しない(__attribute__((no_thread_safety_analysis))
      • arch_spin_lock にロック取得、解放メソッドがないので、分析できないため

検出できる不具合

  • 二重のロック取得を警告してくれる

    int main(void) {
      arch_spin_lock_t lock;
      arch_spin_lock(&lock);
      arch_spin_lock(&lock); // warning: acquiring mutex 'lock' that is already held
      return 0;
    }
    
  • 二重のロック解放を警告してくれる

    int main(void) {
      arch_spin_lock_t lock;
      arch_spin_lock(&lock);
      arch_spin_unlock(&lock);
      arch_spin_unlock(&lock); // warning: releasing mutex 'lock' that was not held
      return 0;
    }
    
  • ロックの解放し忘れを警告してくれる

    int main(void) {
      arch_spin_lock_t lock;
      arch_spin_lock(&lock);
      return 0;
    } // warning: mutex 'lock' is still held at the end of function
    
  • ロック未取得時の解放を警告してくれる

    int main(void) {
      arch_spin_lock_t lock;
      arch_spin_unlock(&lock); // warning: releasing mutex 'lock' that was not held
      return 0;
    }
    
  • arch_spin_trylock() の戻り値判定

    正しい処理
    int main(void) {
      arch_spin_lock_t lock;
      bool success;
      success = !arch_spin_trylock(&lock); // 成功時 false を返す
      if (success) {
        arch_spin_unlock(&lock);
      }
      return 0;
    }
    
    戻り値を判定しないで解放したら警告
    int main(void) {
      arch_spin_lock_t lock;
      bool success;
      success = !arch_spin_trylock(&lock); // 成功時 false を返す
      arch_spin_unlock(&lock); // warning: releasing mutex 'lock' that was not held
      return 0;
    }
    
    戻り値を誤判定して解放したら警告
    int main(void) {
      arch_spin_lock_t lock;
      bool success;
      success = !arch_spin_trylock(&lock); // 成功時 false を返す
      if (success) {
      } else {
        arch_spin_unlock(&lock); // warning: releasing mutex 'lock' that was not held
      }
      return 0;
    }
    
    戻り値を判定して、解放しないと警告
    int main(void) {
      arch_spin_lock_t lock;
      bool success;
      success = !arch_spin_trylock(&lock); // 成功時 false を返す
      if (success) {
      }
      return 0; // warning: mutex 'lock' is not held on every path through here
    }
    
    解放しない場合、警告なし
    int main(void) {
      arch_spin_lock_t lock;
      bool success;
      success = !arch_spin_trylock(&lock); // 成功時 false を返す
      // 警告なし
      return 0;
    }
    

付録

警告一覧

https://clang.llvm.org/docs/DiagnosticsReference.html#wthread-safety

日本語の記事

https://pebble8888.hatenablog.com/entry/2020/01/03/002315

Zircon での使用方法

https://fuchsia.dev/fuchsia-src/development/kernel/threads/thread_annotations?hl=en

Google のペーパー

C/C++ Thread Safety Analysis

Discussion