将互斥锁与其数据相关联的正确方法是什么?

时间:2013-04-05 22:59:16

标签: c++ c++11 mutex

在将资金从一个银行账户转移到另一个银行账户的经典问题中,接受的解决方案(我认为)是将互斥锁与每个银行账户相关联,然后在从一个账户中提取资金并将其存入另一个账户之前锁定这两个账户。 。乍一看,我会这样做:

class Account {
public:
  void deposit(const Money& amount);
  void withdraw(const Money& amount);
  void lock() { m.lock(); }
  void unlock() { m.unlock(); }

private:
  std::mutex m;
};

void transfer(Account& src, Account& dest, const Money& amount)
{
  src.lock();
  dest.lock();

  src.withdraw(amount);
  dest.deposit(amount);

  dest.unlock();
  src.unlock();
}

但手动解锁气味。我可以将互斥锁公开,然后在std::lock_guard中使用transfer,但公共数据成员也会闻到。

std::lock_guard的要求是其类型满足 BasicLockable 要求,这只是对lockunlock的调用有效。 Account符合该要求,因此我可以直接将std::lock_guardAccount一起使用:

void transfer(Account& src, Account& dest, const Money& amount)
{
  std::lock_guard<Account> g1(src);
  std::lock_guard<Account> g2(dest);

  src.withdraw(amount);
  dest.deposit(amount);
}

这似乎没问题,但我以前从未见过这种事情,并且在Account中复制锁定和解锁互斥锁似乎本身就有点臭。

在这样的场景中,将互斥锁与其保护的数据相关联的最佳方法是什么?

更新:在下面的评论中,我注意到std::lock可用于避免死锁,但我忽略了std::lock依赖于try_lock功能的存在(除此之外) lockunlock)。将try_lock添加到Account的界面似乎是一个相当严重的黑客攻击。因此,如果Account对象的互斥锁保留在Account中,则它必须是公共的。这有点恶臭。

一些建议的解决方案让客户端使用包装类来静默地将互斥锁与Account对象相关联,但是,正如我在我的评论中所指出的,这似乎使得代码的不同部分可以轻松地使用不同的Account周围的包装器对象,每个都创建自己的互斥锁,这意味着代码的不同部分可能会尝试使用不同的互斥锁来锁定Account。那很糟糕。

其他提出的解决方案依赖于一次只锁定一个互斥锁。这消除了锁定多个互斥锁的需要,但代价是使某些线程可以看到系统的不一致视图。实质上,这放弃了涉及多个对象的操作的事务语义。

此时,公共互斥体开始看起来像是可用选项中最不臭的,这是我真的不想要的结论。真的没有更好的吗?

8 个答案:

答案 0 :(得分:5)

C ++和2012年之后C++ Concurrency查看 Herb Sutter 。他展示了Monitor Object的例子 - 就像C ++ 11中的实现一样。

monitor<Account> m[2];
transaction([](Account &x,Account &y)
{
    // Both accounts are automaticaly locked at this place.
    // Do whatever operations you want to do on them.
    x.money-=100;
    y.money+=100;
},m[0],m[1]);
// transaction - is variadic function template, it may accept many accounts

实现:

LIVE DEMO

#include <iostream>
#include <utility>
#include <ostream>
#include <mutex>

using namespace std;

typedef int Money;

struct Account
{
    Money money = 1000;
    // ...
};

template<typename T>
T &lvalue(T &&t)
{
    return t;
}

template<typename T>
class monitor
{
    mutable mutex m;
    mutable T t;
public:
    template<typename F>
    auto operator()(F f) const -> decltype(f(t))
    {
        return lock_guard<mutex>(m),
               f(t);
    }
    template<typename F,typename ...Ts> friend
    auto transaction(F f,const monitor<Ts>& ...ms) ->
        decltype(f(ms.t ...))
    {
        return lock(lvalue(unique_lock<mutex>(ms.m,defer_lock))...),
        f(ms.t ...);
    }
};

int main()
{
    monitor<Account> m[2];

    transaction([](Account &x,Account &y)
    {
        x.money-=100;
        y.money+=100;
    },m[0],m[1]);

    for(auto &&t : m)
        cout << t([](Account &x){return x.money;}) << endl;
}

输出是:

900
1100

答案 1 :(得分:1)

将这些钱“飞行”一段时间没有任何问题。这样做:

Account src, dst;

dst.deposit(src.withdraw(400));

现在让每个单独的方法都是线程安全的,例如

int Account::withdraw(int n)
{
    std::lock_guard<std::mutex> _(m_);
    balance -= n;
    return n;
}

答案 2 :(得分:1)

我更喜欢使用非侵入式包装类,而不是使用互斥锁污染原始对象并将其锁定在每个方法调用上。这个包装类(我将其命名为Protected<T>)包含用户对象作为私有变量。 Protected<T>将友谊授予另一个名为Locker<T>的班级。锁定器将包装器作为其构造函数参数,并为用户对象提供公共访问器方法。锁定器还使包装器的互斥锁在其使用寿命期间保持锁定状态。因此,储物柜的生命周期定义了一个可以安全访问原始对象的范围。

Protected<T>可以实现operator->以快速调用单个方法。

工作示例:

#include <iostream>
#include <mutex>


template<typename>
struct Locker;


template<typename T>
struct Protected
{
    template<typename ...Args>
    Protected(Args && ...args) :
        obj_(std::forward<Args>(args)...)
    {        
    }

    Locker<const T> operator->() const;
    Locker<T> operator->();

private:    
    friend class Locker<T>;
    friend class Locker<const T>;
    mutable std::mutex mtx_;
    T obj_;
};


template<typename T>
struct Locker
{
    Locker(Protected<T> & p) :
        lock_(p.mtx_),
        obj_(p.obj_)
    {
        std::cout << "LOCK" << std::endl;
    }

    Locker(Locker<T> && rhs) = default;

    ~Locker()
    {
        std::cout << "UNLOCK\n" << std::endl;
    }

    const T& get() const { return obj_; }
    T& get() { return obj_; }

    const T* operator->() const { return &get(); }
    T* operator->() { return &get(); }

private:    
    std::unique_lock<std::mutex> lock_;
    T & obj_;    
};


template<typename T>
struct Locker<const T>
{
    Locker(const Protected<T> & p) :
        lock_(p.mtx_),
        obj_(p.obj_)
    {
        std::cout << "LOCK (const)" << std::endl;
    }

    Locker(Locker<const T> && rhs) = default;

    ~Locker()
    {
        std::cout << "UNLOCK (const)\n" << std::endl;
    }

    const T& get() const { return obj_; }    
    const T* operator->() const { return &get(); }

private:    
    std::unique_lock<std::mutex> lock_;
    const T & obj_;
};


template<typename T>
Locker<T> Protected<T>::operator->()
{
    return Locker<T>(const_cast<Protected<T>&>(*this));
}


template<typename T>
Locker<const T> Protected<T>::operator->() const
{
    return Locker<T>(const_cast<Protected<T>&>(*this));
}

struct Foo
{
    void bar() { std::cout << "Foo::bar()" << std::endl; }
    void car() const { std::cout << "Foo::car() const" << std::endl; }
};

int main()
{
    Protected<Foo> foo;

    // Using Locker<T> for rw access
    {
        Locker<Foo> locker(foo);
        Foo & foo = locker.get();
        foo.bar();
        foo.car();
    }

    // Using Locker<const T> for const access
    {
        Locker<const Foo> locker(foo);
        const Foo & foo = locker.get();
        foo.car();
    }


    // Single actions can be performed quickly with operator-> 
    foo->bar();
    foo->car();
}

生成此输出:

LOCK
Foo::bar()
Foo::car() const
UNLOCK

LOCK (const)
Foo::car() const
UNLOCK (const)

LOCK
Foo::bar()
UNLOCK

LOCK
Foo::car() const
UNLOCK

Test with online compiler.

更新:修正了const正确性。

PS:还有asynchronous variant

答案 3 :(得分:1)

就个人而言,我是LockingPtr范例的粉丝(这篇文章已经过时了,我不会亲自遵循它的所有建议):

struct thread_safe_account_pointer {
     thread_safe_account_pointer( std::mutex & m,Account * acc) : _acc(acc),_lock(m) {}

     Account * operator->() const {return _acc;}
     Account& operator*() const {return *_acc;}
private:
     Account * _acc;
     std::lock_guard<std::mutex> _lock;
};

并实现包含Account对象的类,如下所示:

class SomeTypeWhichOwnsAnAccount {
public:
     thread_safe_account_pointer get_and_lock_account() const {return thread_safe_account_pointer(mutex,&_impl);}

      //Optional non thread-safe
      Account* get_account() const {return &_impl;}

      //Other stuff..
private:
     Account _impl;
     std::mutex mutex;
};

如果合适,可以用智能指针替换指针,您可能需要const_thread_safe_account_pointer(或者更好的通用模板thread_safe_pointer类)

为什么这比监视器(IMO)更好?

  1. 您可以在不考虑线程安全的情况下设计您的Account类; thread-safety是使用你的类的对象的属性,而不是你自己的类。
  2. 在对类中的成员函数进行嵌套调用时,不需要递归互斥锁。
  3. 您在代码中清楚地记录了是否锁定了互斥锁(并且可以通过不实现get_account来防止完全使用 - 无锁定)。同时使用get_and_lock()get()函数强制来考虑线程安全性。
  4. 在定义函数(全局或成员)时,您有一个干净的语义来指定函数是否需要锁定对象的互斥锁(只传递thread_safe_pointer)或线程安全无关(使用{{ 1}})。
  5. 最后但同样重要的是,Account&与监视器的语义完全不同:
  6. 考虑通过监视器实现线程安全的thread_safe_pointer类,以及以下代码:

    MyVector
    像这样的IMO代码非常糟糕,因为它让你觉得MyVector foo; // Stuff.. , other threads are using foo now, pushing and popping elements int size = foo.size(); for (int i=0;i < size;++i) do_something(foo[i]); 认为监视器会为你处理线程安全,而在这里我们有一个非常难以发现的竞争条件。

答案 4 :(得分:0)

您的问题是将锁定与数据相关联。在我看来,将mutex填入对象中就可以了。您可以更进一步,使对象基本上成为monitors:锁定以进入功能成员,在离开时解锁。

答案 5 :(得分:0)

我相信为每个帐户提供自己的锁定是可以的。它为代码的任何读者提供了一个清晰的信号,即访问Account是一个关键部分。

任何涉及每个帐户一个锁的解决方案的缺点是,当您编写同时操作多个帐户的代码时,您必须注意死锁。但是,避免该问题的直接方法是将您的交互限制为一次只能使用一个帐户。这不仅避免了潜在的死锁问题,而且还增加了并发性,因为当前线程忙于处理不同的事情时,您不会阻止某些其他线程访问其他某个帐户。

您对一致视图的担忧是有效的,但可以通过记录当前事务发生的操作来实现。例如,您可以使用事务日志装饰deposit()withdraw()操作。

class Account {
  void deposit(const Money &amount);
  void withdraw(const Money &amount);
public:
  void deposit(const Money &amount, Transaction& t) {
    std::lock_guard<std::mutex> _(m_);
    deposit(amount);
    t.log_deposit(*this, amount);
  }
  void withdraw(const Money &amount, Transaction& t) {
    std::lock_guard<std::mutex> _(m_);
    withdraw(amount);
    t.log_withdraw(*this, amount);
  }
private:
  std::mutex m_;
};

然后,transfer是记录的提款和存款。

void transfer (Account &src, Account &dest, const Money &amount,
               Transaction &t) {
  t.log_transfer(src, dest, amount);
  try {
    src.withdraw(amount, t);
    dest.deposit(amount, t);
    t.log_transfer_complete(src, dest, amount);
  } catch (...) {
    t.log_transfer_fail(src, dest, amount);
    //...
  }
}

请注意,事务日志的概念与您选择部署锁的方式正交。

答案 6 :(得分:0)

我认为你的答案是按照你的建议去做并使用std :: lock(),但把它放到朋友的函数中。这样您就不需要公开帐户互斥。新朋友功能不使用deposit()和withdraw()函数,需要单独锁定和解锁互斥锁。请记住,朋友功能不是成员函数,但可以访问私有成员。

typedef int Money;

class Account {
public:
  Account(Money amount) : balance(amount)
  {
  }

  void deposit(const Money& amount);
  bool withdraw(const Money& amount);

  friend bool transfer(Account& src, Account& dest, const Money& amount)
  {
     std::unique_lock<std::mutex> src_lock(src.m, std::defer_lock);
     std::unique_lock<std::mutex> dest_lock(dest.m, std::defer_lock);
     std::lock(src_lock, dest_lock);

     if(src.balance >= amount)
     {
        src.balance -= amount;
        dest.balance += amount;
        return true;
     }
     return false;
  }
private:
  std::mutex m;
  Money balance;
};

答案 7 :(得分:0)

大多数解决方案存在数据保持公开的问题,因此可以在不锁定锁的情况下访问它。

有一种方法可以解决这个问题,但是你不能使用模板,因此必须求助于宏。在C ++ 11中实现它更好,而不是在这里重复整个讨论,我链接到我的实现:https://github.com/sveljko/lockstrap