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

前端 未结 8 1404
轮回少年
轮回少年 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:28

    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) contains the user object as a private variable. Protected grants friendship to another class called Locker. 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 can implement operator-> to enable quickly calling a single method.

    Working example:

    #include 
    #include 
    
    
    template
    struct Locker;
    
    
    template
    struct Protected
    {
        template
        Protected(Args && ...args) :
            obj_(std::forward(args)...)
        {        
        }
    
        Locker operator->() const;
        Locker operator->();
    
    private:    
        friend class Locker;
        friend class Locker;
        mutable std::mutex mtx_;
        T obj_;
    };
    
    
    template
    struct Locker
    {
        Locker(Protected & p) :
            lock_(p.mtx_),
            obj_(p.obj_)
        {
            std::cout << "LOCK" << std::endl;
        }
    
        Locker(Locker && 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 lock_;
        T & obj_;    
    };
    
    
    template
    struct Locker
    {
        Locker(const Protected & p) :
            lock_(p.mtx_),
            obj_(p.obj_)
        {
            std::cout << "LOCK (const)" << std::endl;
        }
    
        Locker(Locker && 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 lock_;
        const T & obj_;
    };
    
    
    template
    Locker Protected::operator->()
    {
        return Locker(const_cast&>(*this));
    }
    
    
    template
    Locker Protected::operator->() const
    {
        return Locker(const_cast&>(*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;
    
        // Using Locker for rw access
        {
            Locker locker(foo);
            Foo & foo = locker.get();
            foo.bar();
            foo.car();
        }
    
        // Using Locker for const access
        {
            Locker 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.

提交回复
热议问题