boost线程同步

来源:互联网 发布:淘宝office2016激活码 编辑:程序博客网 时间:2024/06/03 13:14

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

 

 

1 0
原创粉丝点击