vendredi 3 juillet 2015

Shouldn't this code lead to a deadlock?

I have a class which contains a mutex and an object, each time I need to access the contained object, a method is called to lock the mutex and return te contained object, let's see the code:

template <typename MUTEX, typename RESOURCE>
class LockedResource
{
    using mutex_t    = MUTEX;
    using resource_t = RESOURCE;

    mutex_t    m_mutex;
    resource_t m_resource;

public:
        template <typename ... ARGS>
        LockedResource(ARGS &&... args) :
                m_resource(std::forward<ARGS>(args) ...)
        {}

    class Handler
    {
        std::unique_lock<mutex_t> m_lock;      // unique lock
        resource_t                &m_resource; // Ref to resource

        friend class LockedResource;

        Handler(mutex_t &a_mutex, resource_t &a_resource) :
            m_lock(a_mutex),       // mutex automatically locked
            m_resource(a_resource)
        { std::cout << "Resource locked\n"; }

    public:
        Handler(Handler &&a_handler) :
            m_lock(std::move(a_handler.m_lock)),
            m_resource(a_handler.m_resource)
        { std::cout << "Moved\n"; }

        ~Handler() // mutex automatically unlocked
        { std::cout << "Resource unlocked\n"; }

        RESOURCE *operator->()
        { return &m_resource; }
    };

    Handler get()
    { return {m_mutex, m_resource}; }
};

template <typename T> using Resource = LockedResource<std::mutex, T>;

The idea behind this code is to wrap an object and protect it from multiple access from multiple threads; the wrapped object have private visibility and the only way to access it is through the internal class Handler, the expected usage is the following:

LockedResource<std::mutex, Foo> locked_foo;
void f()
{
    auto handler = locked_foo.get(); // this will lock the locked_foo.m_mutex;
    handler->some_foo_method();
    // going out of the scope will call the handler dtor and
    // unlock the locked_foo.m_mutex;
}

So, if I'm not mistaken, calling the LockedResource::get method creates a LockedResource::Handle value which locks the LockedResource::m_mutex for the entire lifetime of the Handle... but I must be mistaken because the code below doesn't cause a deadlock:

LockedResource<std::mutex, std::vector<int>> locked_vector{10, 10};

int main()
{
/*1*/  auto vec = locked_vector.get(); // vec = Resource<vector>::Handler
/*2*/  std::cout << locked_vector.get()->size() << '\n';
/*3*/  std::cout << vec->size() << '\n';
    return 0;
}

I was expecting the line /*1*/ to lock the locked_vector.m_mutex and then the line /*2*/ try to lock the same already locked mutex causing deadlock, but the output is the following:

Resource locked
Resource locked
10
Resource unlocked
10
Resource unlocked

  • Shouldn't the second ::get() lead to a deadlock?
  • I'm accessing the wrapped resource through the same lock or I am misunderstanding something?

Here is the example code.

Aucun commentaire:

Enregistrer un commentaire