boost线程同步

1 篇文章 0 订阅
1 篇文章 0 订阅

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
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值