What's the proper way to associate a mutex with its data?

杀马特。学长 韩版系。学妹 提交于 2021-02-17 19:41:31

问题


In the classic problem of transferring money from one bank account to another, the accepted solution (I believe) is to associate a mutex with each bank account, then lock both before withdrawing the money from one account and depositing it into the other. At first blush, I'd do it like this:

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();
}

But the manual unlocking smells. I could make the mutex public, then use std::lock_guard in transfer, but public data members smell, too.

The requirements for std::lock_guard are that its type satisfy the BasicLockable requirements, which is just that calls to lock and unlock are valid. Account satisfies that requirement, so I could just use std::lock_guard with Account directly:

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);
}

This seems okay, but I've never seen this kind of thing done before, and duplicating the locking and unlocking of a mutex in Account seems kind of smelly in its own right.

What's the best way to associate a mutex with the data it's protecting in a scenario like this?

UPDATE: In comments below, I noted that std::lock can be used to avoid deadlock, but I overlooked that std::lock relies on the existence of try_lock functionality (in addition to that for lock and unlock). Adding try_lock to Account's interface seems like a fairly gross hack. It thus seems that if the mutex for an Account object is to remain in Account, it has to be public. Which has quite the stench.

Some proposed solutions have clients use wrapper classes to silently associate mutexes with an Account object, but, as I've noted in my comments, this seems to make it easy for different parts of the code to use different wrapper objects around Account, each creating its own mutex, and that means that different parts of the code may attempt to lock the Account using different mutexes. That's bad.

Other proposed solutions rely on locking only a single mutex at a time. That eliminates the need to lock more than one mutex, but at the cost of making it possible for some threads to see inconsistent views of the system. In essence, this abandons transactional semantics for operations involving multiple objects.

At this point, a public mutex is beginning to look like the least stinky of the available options, and that's a conclusion I really don't want to come to. Is there really nothing better?


回答1:


Check out Herb Sutter talk at C++ and Beyond 2012: C++ Concurrency. He shows example of Monitor Object-like implementation in 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

Implementation:

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;
}

Output is:

900
1100



回答2:


There's nothing wrong with having the money "in flight" for a while. Make it like so:

Account src, dst;

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

Now just make each individual method thread-safe, e.g.

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



回答3:


I prefer to use a non-intrusive wrapper class instead of polluting the original object with a mutex and locking it on each method call. This wrapper class (which I named Protected<T>) contains the user object as a private variable. Protected<T> grants friendship to another class called Locker<T>. The locker takes the wrapper as its constructor argument and provides public accessor methods to the user object. The locker also keeps the wrapper's mutex locked during its lifetime. So the locker's lifetime defines a scope where the original object can be accessed in a safe way.

The Protected<T> can implement operator-> to enable quickly calling a single method.

Working example:

#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();
}

Which generates this output:

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.

Update: fixed const correctness.

PS: There's also an asynchronous variant.




回答4:


Personally I am a fan of the LockingPtr paradigm (this article is quite outdated and I won't personally follow all of its advice) :

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;
};

And implement the classes which contains an Account object like this:

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;
};

Pointers may be replaced with smart pointers if suitable, and you will probably need a const_thread_safe_account_pointer (or even better a general purpose template thread_safe_pointer class)

Why is this better than monitors (IMO)?

  1. You can design your Account class without thinking about thread-safety; thread-safety is a property of the object which uses your class, not of your class itself.
  2. No need for recursive mutexes when nesting calls to member functions in your class.
  3. You document clearly in your code whether you are locking a mutex or not (and you can prevent completely using-without-locking by not implementing get_account). Having both a get_and_lock() and a get() function forces you to think about thread-safety.
  4. When defining functions (global or member), you have a clean semantic to specify whether a function requires the object's mutex to be locked (just pass a thread_safe_pointer) or is thread-safety-agnostic (use Account&).
  5. Last but not least, thread_safe_pointer has a quite different semantic from monitors:

Consider a MyVector class which implements thread-safety via monitors, and the following code:

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]);

IMO code like this is really bad, because it makes you feel safe thinking that monitors will take care of thread-safety for you, while here we have a race condition which is incredibly difficult to spot.




回答5:


Your problem is to associate the locking with the data. In my eyes, stuffing the mutex into the object is fine. You could go even further, making the objects essentially into monitors: Lock to enter a function member, unlock when leaving.




回答6:


I believe providing each account with its own lock is fine. It provides a clear signal to any reader of your code that accessing the Account is a critical section.

The downside to any solution involving one lock per account is that you have to be mindful of deadlock when you are writing code that manipulates multiple accounts simultaneously. But, the straightforward way to avoid that problem is to limit your interactions to be with one account at a time. This not only avoids potential deadlock problems, it also increases concurrency, since you are not blocking some other thread from being able to access some other account while the current thread is busy with something different.

Your concerns about a consistent view is valid, but can be achieved by logging the operations that are occurring with the current transaction. For example, you can decorate your deposit() and withdraw() operations with a transaction log.

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_;
};

Then, a transfer is a logged withdrawal and deposit.

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);
    //...
  }
}

Note that the idea of a transaction log is orthogonal to how you choose to deploy your locks.




回答7:


I think your answer is to do as you suggest and use std::lock(), but put it into a friend function. That way you don't need to make the account mutex public. The deposit() and withdraw() functions are not used by the new friend function and will need to separately lock and unlock the mutex. Remember that friend functions are not member functions but do have access to private members.

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;
};



回答8:


Most of the solutions have a problem that data is kept public, thus one can access it without locking the lock.

There is a way to fix that, but you can't use templates and thus have to resort to macros. It is much nicer to implement in C++11 and rather then repeating the whole discussion here, I link to my implementation on: https://github.com/sveljko/lockstrap



来源:https://stackoverflow.com/questions/15844972/whats-the-proper-way-to-associate-a-mutex-with-its-data

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!