实例开篇
线程安全注解是现代C++开发的机制,通常在编译期可以帮助发现一些线程安全问题。
下面直接从实例中来理解。
cpp
class Account {
private:
Mutex mu;
int money GUARDED_BY(mu);
void Sub(int amount) {
money -= amount; // writing variable 'money' requires holding mutex 'mu' exclusively
}
void Add(int amount) REQUIRES(mu) {
money += amount;
}
public:
void Withdraw(int amount) {
mu.Lock();
Sub(amount); // warning: mutex 'mu' is still held at the end of function
}
void Deposit(int amount) {
mu.Lock();
Add(amount);
mu.Unlock();
}
void TransferFromOthers(Account& b, int amount) {
mu.Lock();
Sub(amount);
b.Add(amount); // calling function 'Add' requires holding mutex 'b.mu' exclusively
mu.Unlock();
}
};
这段是一个账户的例子。
接口
向外暴露的接口为:
-
void Withdraw(int amount)取款取款调用了
void Sub(int amount)而这里在
Withdraw函数中也确实在调用Sub函数前已经持有锁mu了,但是这里并未对Sub函数添加REQUIRES(mu)注解,所以Sub函数本身不能保证在此已经持有锁mu,所以触发 Warningmutex 'mu' is still held at the end of function但是在
Sub函数结束后,意味着需要释放锁,否则其他操作将无法获得锁mu,造成死锁问题。 -
void Deposit(int amount)存款存款调用了
void Add(int amount)在进入
Add函数前持有了锁mu,在从Add函数返回后也释放了锁mu,所以无报错。对于
Add函数,其中添加了REQUIRES(mu)注解,如果存在某个调用Add的函数未在进入Add函数前持有锁mu,则会在调用Add函数的部分触发 Warningcalling function 'Add' requires holding mutex 'mu' exclusively。如下代码所示cppvoid Deposit(int amount) { Add(amount); // calling function 'Add' requires holding mutex 'mu' exclusively } -
void TransferToOthers(Account& b, int amount)当前账号向账户 b 转账这个操作应该是个原子操作,这里先是减少当前账户余额,然后增加账户 b 的余额。
Sub需要加锁,加的是当前账户的锁mub.Add也需要加锁,加的是账户 b 的锁b.mu但是这里在进入
b.Add函数前,当前函数并未持有锁b.mu,所以触发 Warningcalling function 'Add' requires holding mutex 'b.mu' exclusively
如何修改呢,按照上述所述修改即可
cpp
private:
Mutex mu;
int money GUARDED_BY(mu);
void Sub(int amount) REQUIRES(mu) {
money -= amount;
}
void Add(int amount) REQUIRES(mu) {
money += amount;
}
public:
void Withdraw(int amount) {
mu.Lock();
Sub(amount);
mu.Unlock();
}
void Deposit(int amount) {
mu.Lock();
Add(amount);
mu.Unlock();
}
void TransferToOthers(Account& b, int amount) {
mu.Lock();
Sub(amount);
b.mu.Lock();
b.Add(amount);
b.mu.Unlock();
mu.Unlock();
}
};
线程安全注解
这其中涉及到两个注解:
GUARDED_BY(mu),用于标识共享变量的保护互斥量。表示使用该变量时必须获得锁muREQUIRES(mu),用于标识函数在调用时需要独占地持有指定的互斥量。表示进入该函数前必须获得锁mu
在多线程编程中,当多个线程同时访问共享资源时,需要确保某些操作的执行是互斥的,即同一时间只能由一个线程执行。互斥操作通常涉及对共享资源的修改或对多个资源的原子性操作。
Mutex 定义
cpp
class LOCKABLE Mutex {
public:
Mutex() = default;
~Mutex() = default;
Mutex(const Mutex&) = delete;
Mutex& operator=(const Mutex&) = delete;
void Lock() EXCLUSIVE_LOCK_FUNCTION() { mu_.lock(); }
void Unlock() UNLOCK_FUNCTION() { mu_.unlock(); }
void AssertHeld() ASSERT_EXCLUSIVE_LOCK() {}
private:
std::mutex mu_;
};
这里涉及到了四个注解:
-
LOCKABLE一个自定义的宏定义或类型定义,表示这是一个可被加锁的类 -
EXCLUSIVE_LOCK_FUNCTION()表示被标记的函数是一个独占锁函数,调用这个函数会获取一个独占锁。 -
UNLOCK_FUNCTION()表示被标记的函数是一个解锁函数,调用这个函数会释放之前获取的独占锁。 -
ASSERT_EXCLUSIVE_LOCK()表示被标记的函数用于断言当前线程持有一个独占锁,确保调用该函数的线程在调用时持有独占锁。