📚
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