Pebble Coding

ソフトウェアエンジニアによるIT技術、数学の備忘録

C++でstd::mutexの使い方の誤りを検出するThread Safety Analysis

C++でstd::mutexの使い方の誤りを検出するThread Safety Analysisというのがclangにあるそうです。

Thread Safety Analysis — Clang 10 documentation

bitcoin coreのソースで使われていて知ったのですが、xcode でも使えました。
導入手順
1. 192行の mutex.h をインクルードする。
2. std::mutex の代わりに mutex.hに定義されたMutexを使う。
3. Other C++ Flags に "-Wthread-safety" を指定する。

コンパイル実行結果を先に示しておきます。

/Users/pebble8888/Desktop/a/a/main.cpp:16:5: warning: writing variable 'balance' requires holding mutex 'mu' exclusively [-Wthread-safety-analysis]
    balance += amount;       // WARNING! Cannot write balance without locking mu.
    ^
/Users/pebble8888/Desktop/a/a/main.cpp:27:3: warning: mutex 'mu' is still held at the end of function [-Wthread-safety-analysis]
  }                          // WARNING!  Failed to unlock mu.
  ^
/Users/pebble8888/Desktop/a/a/main.cpp:25:8: note: mutex acquired here
    mu.Lock();
       ^
/Users/pebble8888/Desktop/a/a/main.cpp:31:7: warning: calling function 'withdrawImpl' requires holding mutex 'b.mu' exclusively [-Wthread-safety-precise]
    b.withdrawImpl(amount);  // WARNING!  Calling withdrawImpl() requires locking b.mu.
      ^
/Users/pebble8888/Desktop/a/a/main.cpp:31:7: note: found near match 'mu'
3 warnings generated.


ソースコードはこちらです。

#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();
    }
};

まず、アクセスする時に必ずmutexでロックするべき変数にGUARDED_BY()を宣言しています。
どのmutexを使うかも指定します。
指定のmutexをロックせずにこの変数にアクセスしている箇所はwarningが出力されます。

mutexのロックを解除せずに抜けている箇所にはwarningが出力されます。
安全にロック、アンロックする場合はstd::lock_gaurdを使うのが常套手段ですが、
このMutexを使用した場合は、mutex.h内に、同等の機能である MutexLocker があるのでこれを使うようです。

関数にREQUIRESをつけた場合は、この関数内でbalanceにアクセスしてもwarningが出なくなります。
その代わり、この関数を呼び出す時にLockされていないとwarningが出ます。

サンプルコードに載っていない他のマクロについてもみていきます。

GUARDED_BY(x), PT_GUARDED_BY(x)
PT_GUARDED_BY(x) は GUARDED_BY(x)と同じですが、ポインターやスマートポインタに対して使うようです。

REQUIRES(...), REQUIRES_SHARED(...)
必要なmutexを引数に指定します。

ACQUIRE(...), ACQUIRE_SHARED(...), RELEASE(...), RELEASE_SHARED(...)
この関数内でロックされていないといけない、またアンロックされていないといけないという意味です。

TRY_ACQUIRE(...), TRY_ACQUIRE_SHARED(...)
TryLock用です。

EXCLUDES(...)
この関数に入る時にアンロック状態でないといけないという意味です。

NO_THREAD_SAFETY_ANALYSIS
チェックをオフするという意味です。

ACQUIRED_BEFORE(...), ACQUIRED_AFTER(...)
2つ以上のmutexのロック順序を記述します。

RETURN_CAPABILITY(x)
mutexを関数で返す場合に指定します。

CAPABILITY(x)
クラスに対して指定します。

SCOPED_CAPABILITY
クラスに対して指定します。

ASSERT_CAPABILITY(x), ASSERT_SHARED_CAPABILITY(x)
capabilityをもつクラスかどうかをassertします。