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

前端 未结 8 1406
轮回少年
轮回少年 2021-02-05 23:44

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 b

8条回答
  •  小鲜肉
    小鲜肉 (楼主)
    2021-02-06 00:20

    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 _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.

提交回复
热议问题