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相对于一种激活钥匙。