📚
Clang のスレッドセーフ分析
Zircon のスピンロック実装を読んでいたときに出てきた、Clang
の Thread Safety Analysis
について紹介します。
スレッドセーフ分析とは
Clang Thread Safety Analysis は、コード内の潜在的な競合状態について警告する C ++言語拡張機能です。分析は完全に静的です(つまり、コンパイル時)。実行時のオーバーヘッドはありません。
何をすれば、何を警告してくれるのか
-
排他制御用のクラスを作成する
class __attribute__((capability("mutex"))) Mutex { public: void Lock() __attribute__((acquire_capability())) {/* 実装省略 */}; void Unlock() __attribute__((release_capability())) {/* 実装省略 */}; };
-
Mutex
クラスにケーパビリティmutex
を関連つける(__attribute__((capability("mutex")))
) -
Lock()
メソッドはケーパビリティを取得する(__attribute__((acquire_capability()))
) -
Unlock()
メソッドはケーパビリティを解放する(__attribute__((release_capability()))
)
-
-
リソースに、排他制御用のオブジェクトを関連つける
Mutex mu; // 排他制御用のオブジェクト int resource __attribute__((guarded_by(mu))) = 0; // リソース
- リソース
resource
に、排他制御用のオブジェクトmu
を関連つける(__attribute__((guarded_by(mu)))
)
resource
はmu
で守られる -
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.h、spinlock.cc)に出てくる属性を中心に紹介します。
__attribute__((capability("mutex")))
__attribute__((acquire_capability(lock)))
__attribute__((try_acquire_capability(lock)))
__attribute__((release_capability(lock)))
__attribute__((no_thread_safety_analysis))
使い方
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;
-
arch_spin_lock
構造体にケーパビリティmutex
を関連つける(__attribute__((capability("mutex")))
) -
Lock()
、Unlock()
メソッドは実装しない
-
-
ケーパビリティの扱いを、関数に指定する
-
ロックを取得するまでループする関数
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; }
付録
警告一覧
日本語の記事
Zircon での使用方法
Discussion