boost线程同步
l 内锁
l 外锁
除了C++11标准提供的锁,boost.thread提供了其他额外的锁和工具。
内锁:
考虑一个银行账户class,该class提供存款和取款从不同的地点。(可以说是多线程编程中的”Hello, World”)
该账户的姓名为Joe。 另外有两个线程,一个线程为银行代理,该代理负责将钱存入Joe的账户,也就是说它代表Joe替Joe存钱,不用Joe自己去手动操作。而另一个线程取钱,Joe自己动手从账户中取,也只有他自己有权取的出来。
class BankAccount;
BankAccount JoesAccount;
void bankAgent()
{
for (int i=10; i>0;--i) {
//...
JoesAccount.Deposit(500);
//...
}
}
void Joe(){
for (int i=10; i>0;--i) {
//...
int myPocket = JoesAccount.Withdraw(100);
std::cout << myPocket << std::endl;
//...
}
}
int main(){
//...
boost::thread thread1(bankAgent);// start concurrent execution ofbankAgent
boost::thread thread2(Joe);// start concurrent execution of Joe
thread1.join();
thread2.join();
return 0;
}
两个线程并发执行,代理每次存入$500,存10次,Joe每次取$100,取10次。
只要代理和Joe不同时操作账户。上面的代码可以正确的运行。但是谁也不能保证。所以,我们打算使用互斥量保证对账户的操作时是排他性的(独占)。
class BankAccount {
boost::mutex mtx_;
int balance_;
public:
void Deposit(int amount) {
mtx_.lock();
balance_ += amount;
mtx_.unlock();
}
void Withdraw(int amount) {
mtx_.lock();
balance_ -= amount;
mtx_.unlock();
}
int GetBalance() {
mtx_.lock();
int b = balance_;
mtx_.unlock();
return b;
}
};
这样,保证了存取款操作不会被同时执行。
互斥量是一种简单基础的机制,保证同步执行。在上面的例子中操作同步执行是很容易令人信服的(在不出现异常的情况下)。但是,在一个复杂的并发与共享的环境中,大量的使用互斥量将使得使用繁琐,可读性也差。取而代之,对于复杂的同步,我们引入一系列的通用类型。
利用RAII我们能够简化范围性的锁定。下面的代码,哨兵(lock_guard)锁的构造函数会锁传入的互斥量。析构时,解锁互斥量。
class BankAccount {
boost::mutex mtx_; // explicit mutex declaration
int balance_;
public:
void Deposit(int amount) {
boost::lock_guard<boost::mutex> guard(mtx_);
balance_ += amount;
}
void Withdraw(int amount) {
boost::lock_guard<boost::mutex> guard(mtx_);
balance_ -= amount;
}
int GetBalance() {
boost::lock_guard<boost::mutex> guard(mtx_);
return balance_;
}
};
然而,仅仅使用这种内部的对象级别的锁(object-level),可能还不能满足复杂的业务需求。比如,上面的模型是很容易发生死锁的。尽量如此,对象级别的锁还是很有用的在许多情况下。将其与其他机制结合起来,可以为多线程访问提供满意的解决方案。
上面的BankAccount 使用了内部锁定。这种对象级的内部锁定,保证了对该对象的成员函数的访问是线程同步的,不会把这个对象搞死。这种方式,对于给定的类的某个对象,在同一时刻,有且只有一个成员函数在执行,所有成员函数的调用将是序列化的。
这种方法是合理的,且易于实现的。但是不幸的是,”简单”有时可能意味着可能过于简单了(出现问题了呗)。当然并不是说这个方法有问题,而是业务逻辑复杂,仅仅使用这种方式,是不够的。
仅仅使用这种内部锁,不足以应付真实世界。我们上面的例子是简单的模型,想象一下,ATM取款的扣款是两步的,一步取你输入的提款额,另一部分扣除手续费$2,那么取款操作必须是序列化的。
下面的实现,明显的不妥的:
void ATMWithdrawal(BankAccount& acct, int sum) {
acct.Withdraw(sum);
// preemption possible
acct.Withdraw(2);
}
问题在于,两次调用之间,可能有另外一个线程对账户执行了另外的操作。从而破坏了上面的设计要求。
尝试解决这个问题,我们打算对这两个操作整体加锁。
void ATMWithdrawal(BankAccount& acct, int sum) {
boost::lock_guard<boost::mutex> guard(acct.mtx_); 1
acct.Withdraw(sum);
acct.Withdraw(2);
}
注意到这段不能通过编译,因为acct.mtx_是对象私有的,不能在对象外面访问。两种方式解决:
- 使mtx_成为public,看着比较反常
- 添加lock/unlock函数,使得BankAccount可锁
class BankAccount {
boost::mutex mtx_;
int balance_;
public:
void Deposit(int amount) {
boost::lock_guard<boost::mutex> guard(mtx_);
balance_ += amount;
}
void Withdraw(int amount) {
boost::lock_guard<boost::mutex> guard(mtx_);
balance_ -= amount;
}
void lock() {
mtx_.lock();
}
void unlock() {
mtx_.unlock();
}
};
或者从有这两个函数的类继承。
class BankAccount
: public basic_lockable_adapter<mutex>
{
int balance_;
public:
void Deposit(int amount) {
boost::lock_guard<BankAccount> guard(*this);
balance_ += amount;
}
void Withdraw(int amount) {
boost::lock_guard<BankAccount> guard(*this);
balance_ -= amount;
}
int GetBalance() {
boost::lock_guard<BankAccount> guard(*this);
return balance_;
}
};
ATM取款变成:
void ATMWithdrawal(BankAccount& acct, int sum) {
boost::lock_guard<BankAccount> guard(acct);
acct.Withdraw(sum);
acct.Withdraw(2);
}
现在账户acct先被guard对象加锁,然后又被提款操作再次加锁。可能发生下面两种事情:
- 你使用的互斥量实现上可能支持同一线程对同一锁多次加锁(递归锁)。如果是递归式锁,上述代码正常工作,但是明显的,第二次提款的加锁动作是没必要的,这带来了一定的性能损失
- 你使用的锁不是递归式的,那么在第二次加锁后,将自己阻塞自己,死锁。
因为boost::mutex不是递归式的,因此我们需要使用它的递归版本boost::recursive_mutex
class BankAccount
: public basic_lockable_adapter<recursive_mutex>
{
// ...
};
注意,让客户(调用方)手动自己去加解锁,比较方便灵活,但是这是将责任推给了客户,如果客户不正确的使用。比如,为了存款加外锁,但是之后忘记了解锁。或者之前的例子一样,ATM取款,没有将两次取款整体加锁。或者使用了boost::mutex导致死锁。
如果你的类,在使用中,需要客户来保证,即对外界代码要求过分,过于依赖外界。那说明你设计的类,封装的不好。
总结:1.内锁效率损失或者遇到不是递归式锁将死锁。2.外部加锁过于依赖调用方来保证正确性,它回避问题,将责任丢给外界,使得类的使用容易出现错误。
外锁:
基于上一节的讨论,理想的BankAccount 应该像下面这样:
- 支持两种锁模型(internal and external)
- 高效的;即不要在没必要的加锁的情况下,还去加锁
- 安全的,即在没有得到合适的锁情况下,不能操作账户
让我们做一个有价值的思考:每当你要锁定一个BankAccount,你都使用一个lock_guard<BankAccount> 对象去锁定。反过来说,哪里有lock_guard<BankAccount>对象,哪里就有一个BankAccount存在。因此,你可以将lock_guard<BankAccount>对象看做一种许可,拥有该对象表明你有权限对账户操作。lock_guard<BankAccount>对象应该是不可拷贝和别名的。
1. 只要许可仍然活着,某个BankAccount仍然被锁住的
2. 当许可lock_guard<BankAccount>销毁,BankAccount的互斥量将释放
现在,我们打算对之前的boost.thread中的模板lock_guard进行增强。我们将增强的版本叫做strict_lock。实质上,strict_lock的角色只是栈中的变量。strict_lock必须是不可拷贝的、不可别名的。strict_lock通过将拷贝构造函数和赋值函数声明为private的来禁止复制行为。
template <typename Lockable>
class strict_lock {
public:
typedef Lockable lockable_type;
explicit strict_lock(lockable_type& obj) : obj_(obj) {
obj.lock(); // locks on construction
}
strict_lock() = delete;
strict_lock(strict_lock const&) = delete;
strict_lock& operator=(strict_lock const&) = delete;
~strict_lock() { obj_.unlock(); } // unlocks on destruction
bool owns_lock(mutex_type const* l) const noexcept // strict lockers specific function
{
return l == &obj_;
}
private:
lockable_type& obj_;
};
沉默有时胜于言语,对于strict_lock,什么不能做与什么能做一样的重要。让我们看看,当实例化一个strict_lock时,你能做什么,以及你不能做什么:
- 你必须显示的使用有效的T对象,实例化strict_lock<T>,这是实例化它的唯一方式
BankAccount myAccount("John Doe", "123-45-6789");
strict_lock<BankAccount> myLock(myAccount); // ok
- 你不能将其拷贝给其他。特别的,你不能以传值的方式传给函数,以及从函数返回
- 不过,你仍然可以通过传递引用进出函数
// ok, Foo returns a reference to strict_lock<BankAccount>
extern strict_lock<BankAccount>& Foo();
// ok, Bar takes a reference to strict_lock<BankAccount>
extern void Bar(strict_lock<BankAccount>&);
所有这些规则都为了保证,当你持有一个strict_lock<T>对象时,你锁住了T对象,并且在之后的某个点,T对象会被解锁。
现在我们有了strict_lock,如何利用它为BankAccount定义一个安全、灵活的接口呢?思路如下:
- BankAccount的接口函数(在本例中,Deposit和Withdraw)有两种形式的重载
- 一种与原来的(函数签名式)相同,另一种这多了一个strict_lock<BankAccount>参数,第一种使用内部锁;第二种需要一个外部锁,这个外部锁需要客户在编译期就提供的,用户代码中创建strict_lock<BankAccount>对象。
- BankAccount通过将内部加锁的函数转发给外部加锁的函数避免了代码膨胀。真正的工作是由外部加锁的那个函数做的。
俗话说的好,一小段代码胜过千言万语,下面是新的BankAccount:
class BankAccount
: public basic_lockable_adapter<boost::mutex>
{
int balance_;
public:
void Deposit(int amount, strict_lock<BankAccount>&) {
// Externally locked
balance_ += amount;
}
void Deposit(int amount) {
strict_lock<BankAccount> guard(*this); // Internally locked
Deposit(amount, guard);
}
void Withdraw(int amount, strict_lock<BankAccount>&) {
// Externally locked
balance_ -= amount;
}
void Withdraw(int amount) {
strict_lock<BankAccount> guard(*this); // Internally locked
Withdraw(amount, guard);
}
};
现在,你如果仅仅想使用内锁,Deposit(int)和Withdraw(int)。想使用外锁,你可以在外面创建strict_lock<BankAccount>接下来调用第二种两个参数版本,Deposit(int, strict_lock<BankAccount>&)。举例,下面是一个ATMWithdrawal函数的正确实现:
void ATMWithdrawal(BankAccount& acct, int sum) {
strict_lock<BankAccount> guard(acct);
acct.Withdraw(sum, guard);
acct.Withdraw(2, guard);
}
这种方式不但相对的安全,而且没有多余的加锁动作。
值得注意的是,以模板实现的strict_lock比起直接用继承实现,能提供更高的安全。使用继承的方式,strict_lock继承基类的锁接口,需要调用锁接口(如果调用出现异常,上锁失败),但是模板不需要,不过继承比模板需要编译时间更少。继承方式,让我们知道有某个从锁类继承而来的某个类对象现在被锁住了,而模板strict_lock<BankAccount>,当你拥有这么个对象时候,表明有某个BankAccount已经处于锁定状态了(strict_lock的构造传递的是引用)。相当于一个是进行时ing,一个是过去式done。
注意到我的言辞,我提及到ATMWithdrawal是相对安全的。实际上它不是真的绝对安全的,因为strict_lock<BankAccount>只表明了你要锁的对象的类型(BankAccount),类型系统只是确保某个BankAccount对象被锁住了,并没有限定究竟是哪个具体的BankAccount对象被锁定。比如,考虑下面的蓄意构造的假实现:
void ATMWithdrawal(BankAccount& acct, int sum) {
BankAccount fakeAcct("John Doe", "123-45-6789");
strict_lock<BankAccount> guard(fakeAcct);
acct.Withdraw(sum, guard);
acct.Withdraw(2, guard);
}
上面的代码不会产生任何警告,顺利通过编译,但是明显的,它没有正确的工作,它本是想在操作账户前锁住需要被操作的账户,但是它却锁了一个账户,却去操作另一个账户,这显然不是我们想要的。
如果我们的设计还需要运行时检查,那就显得不方便实用了。
粗心或恶意的程序猿可能操作任何银行账户。
C这种语言,需要程序员小心翼翼和守纪律,C++则稍微好点,不过它仍然是坚持信任程序员的行为。C/C++并没有考虑程序员的恶意行为(不像Jave之类的)。当然你也可以打破语言最初的设计初衷,通过”适当的”手法。
忘记加锁的可能性,相比于知道要加锁,但是锁错了对象的可能性要大的多。
使用strick_lock许可证在编译器查出更多常见的错误,让不怎么会出现的错误在运行时检查。
让我们看看如何实现,首先我们需要向模板类strict_lock添加成员函数bool strict_lock<T>::owns_lock(Lockable*),它返回被锁住的对象的引用。
template <class Lockable> class strict_lock {
... as before ...
public:
bool owns_lock(Lockable* mtx) const { return mtx==&obj_; }
};
接着,BankAccount需要使用这个接口比较
class BankAccount {
: public basic_lockable_adapter<boost::mutex>
int balance_;
public:
void Deposit(int amount, strict_lock<BankAccount>& guard) {
// Externally locked
if (!guard.owns_lock(*this))
throw "Locking Error: Wrong Object Locked";
balance_ += amount;
}
// ...
};
整个这种方法的开销远远低于使用递归锁,去锁第二次。
改善外锁
现在假设BankAccount不在使用它自己的锁,并且在单线程中执行。
class BankAccount {
int balance_;
public:
void Deposit(int amount) {
balance_ += amount;
}
void Withdraw(int amount) {
balance_ -= amount;
}
};
如果你要在多线程环境使用,请使用自己的同步方法在接下来的例子中。
现在有个类AccountManger,它持有账户并操作账户。
class AccountManager
: public basic_lockable_adapter<boost::mutex>
{
BankAccount checkingAcct_;
BankAccount savingsAcct_;
...
};
进一步假设,设计上要求我们在操作AccountManager中的账户时,必须先锁住该管理类。 对于这个设计需求,我们该怎么用C++表达呢?
解决办法是,使用一个桥型模板externally_locked,用来控制对账户的访问。
template <typename T, typename Lockable>
class externally_locked {
BOOST_CONCEPT_ASSERT((LockableConcept<Lockable>));
public:
externally_locked(T& obj, Lockable& lockable)
: obj_(obj)
, lockable_(lockable)
{}
externally_locked(Lockable& lockable)
: obj_()
, lockable_(lockable)
{}
T& get(strict_lock<Lockable>& lock) {
#ifdef BOOST_THREAD_THROW_IF_PRECONDITION_NOT_SATISFIED
if (!lock.owns_lock(&lockable_)) throw lock_error(); //run time check throw if not locks the same
#endif
return obj_;
}
void set(const T& obj, Lockable& lockable) {
obj_ = obj;
lockable_=lockable;
}
private:
T obj_;
Lockable& lockable_;
};
externally_locked为T类型的对象披了层外套,现在访问T对象需要使用get、set方法,并且同时传递一个对该对象的锁。
现在,我们将checkingAcct_和savingsAcct_原来的BankAccount类型,换成externally_locked<BankAccount, AccountManager>:
class AccountManager
: public basic_lockable_adapter<boost::mutex>
{
public:
typedef basic_lockable_adapter<boost::mutex> lockable_base_type;
AccountManager()
: checkingAcct_(*this)
, savingsAcct_(*this)
{}
inline void Checking2Savings(int amount);
inline void AMoreComplicatedChecking2Savings(int amount);
private:
externally_locked<BankAccount, AccountManager> checkingAcct_;
externally_locked<BankAccount, AccountManager> savingsAcct_;
};
现在的模型正是我们需要的——想要访问账户,现在需要调用get,而调用get你必须传递strict_lock<AccountManager>,也就是说你必须先锁住AccountManager。不过有一件事,你必须小心,不要将get得到的引用保存下来使用,如果这样的话,当引用所指的对象销毁了,你的访问将出现问题,尽量不要这么做,我们的原则是尽量的让编译器做更多的事情,而降低自己需要投入的注意力,这能减轻我们的负担。
典型的,像下面这样去使用externally_locked,假设你要原子性的从checking账户转账到savings账户:
void AccountManager::Checking2Savings(int amount) {
strict_lock<AccountManager> guard(*this);
checkingAcct_.get(guard).Withdraw(amount);
savingsAcct_.get(guard).Deposit(amount);
}
我们实现了两个重要的目标,第一,可读性更好,从表面形式看,我们容易看出变量被锁保护。第二,这种设计,你不上锁就不可能访问得到账户,externally_locked相对于一种激活钥匙。