c++ - What's the proper way to associate a mutex with its data? -
in classic problem of transferring money 1 bank account another, accepted solution (i believe) associate mutex each bank account, lock both before withdrawing money 1 account , depositing other. @ first blush, i'd 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 manual unlocking smells. make mutex public, use std::lock_guard in transfer, public data members smell, too.
the requirements std::lock_guard type satisfy basiclockable requirements, calls lock , unlock valid. account satisfies requirement, use std::lock_guard 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, i've never seen kind of thing done before, , duplicating locking , unlocking of mutex in account seems kind of smelly in own right.
what's best way associate mutex data it's protecting in scenario this?
update: in comments below, noted std::lock can used avoid deadlock, overlooked std::lock relies on existence of try_lock functionality (in addition lock , unlock). adding try_lock account's interface seems gross hack. seems if mutex account object remain in account, has public. has quite stench.
some proposed solutions have clients use wrapper classes silently associate mutexes account object, but, i've noted in comments, seems make easy different parts of code use different wrapper objects around account, each creating own mutex, , means different parts of code may attempt lock account using different mutexes. that's bad.
other proposed solutions rely on locking single mutex @ time. eliminates need lock more 1 mutex, @ cost of making possible threads see inconsistent views of system. in essence, abandons transactional semantics operations involving multiple objects.
at point, public mutex beginning least stinky of available options, , that's conclusion don't want come to. there nothing better?
check out herb sutter talk @ c++ , beyond 2012: c++ concurrency. shows example of monitor object-like implementation in c++11.
monitor<account> m[2]; transaction([](account &x,account &y) { // both accounts automaticaly locked @ place. // whatever operations want on them. x.money-=100; y.money+=100; },m[0],m[1]); // transaction - variadic function template, may accept many accounts implementation:
#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
Comments
Post a Comment