Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

c++

c++11

mutex

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?

like image 845
KnowItAllWannabe Avatar asked Apr 05 '13 22:04

KnowItAllWannabe


People also ask

What is a mutex and how does it work?

Mutex is a synchronization primitive that grants exclusive access to the shared resource to only one thread. If a thread acquires a mutex, the second thread that wants to acquire that mutex is suspended until the first thread releases the mutex.

What is the data type of mutex?

The mutex ID is an opaque object; its type is pthread_mutex_t. In AIX, the pthread_mutex_t data type is a structure; on other systems, it might be a pointer or another data type. A mutex must be created once.

How is mutex used?

In computer programming, a mutex (mutual exclusion object) is a program object that is created so that multiple program thread can take turns sharing the same resource, such as access to a file.

Which of the following is the correct syntax to initialize mutex locks to achieve synchronization Mcq?

A mutex is initialized and then a lock is achieved by calling the following two functions : int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); int pthread_mutex_lock(pthread_mutex_t *mutex);


3 Answers

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
like image 193
Evgeny Panasyuk Avatar answered Oct 24 '22 16:10

Evgeny Panasyuk


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;
}
like image 29
Kerrek SB Avatar answered Oct 24 '22 16:10

Kerrek SB


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.

like image 24
StackedCrooked Avatar answered Oct 24 '22 14:10

StackedCrooked