c++multithreadingdesign-patternsshared-ptrreference-counting

Design pattern for constructing resource at first thread arrival and destructing on last thread departure


I'm looking for a concurrency design pattern in C++ which enables a single resource instance to be constructed by the first thread which needs it, persist as long as at least one thread is accessing it (even if it isn't the creating thread), and be automatically destructed when the last thread accessing it is done (i.e. in an RAII fashion when the last reference to the resource goes out of scope).

A practical analogy is how the first person (thread) to enter a room turns on the lights (constructs the resource) and the last person to leave the room turns off the lights (destructs the resource). As long as at least one person is in the room, the lights stay on (persistence). The lights cannot be turned on once they are already turned on (single resource instance). Two or more people (separate threads) cannot be flipping the switch on or off concurrently (thread safety). The lights can be turned back on after already having been turned off in a previous on-off cycle (multi-use, not call_once).

My initial attempt below uses the reference counting nature of std::shared_ptr within a factory method guarded by a mutex, but I cannot seem to get the deleter part of it to work at the correct time and destruct my resource when desired.

Here is my code for starters (but I acknowledge that I may be way off and missing some other common and elegant pattern/solution to this problem). Thanks!

#include <iostream>
#include <memory>
#include <mutex>

struct Lights {
    Lights(){ std::cout << "LIGHTS TURNED ON\n"; }
    ~Lights(){ std::cout << "LIGHTS TURNED OFF\n\n"; }
};

template <class R>
class Room {
public:
    std::shared_ptr<R> Enter() {   // factory method
        std::lock_guard<std::mutex> lock(resourceMutex);
        if (!resource) {
            resource = std::shared_ptr<R>(new R, [this](R* ptr){ Deleter(ptr); });
        }
        else {
            std::cout << "  ...lights already on\n";
        }

        // Copy the shared_ptr (increase the reference count)
        return resource;
    }

private:
    std::mutex resourceMutex;
    std::shared_ptr<R> resource{nullptr};

    void Deleter(R* ptr){
        std::lock_guard<std::mutex> lock(resourceMutex);
        std::cout << "Deleter called.\n";
        delete ptr;
    }
};

int main() {
    Room<Lights> room;
    auto sp1 = room.Enter();
    auto sp2 = room.Enter();
    auto sp3 = room.Enter();
    sp1.reset();
    sp2.reset();
    sp3.reset();

    std::cout << "END of program.\n";
    return 0;
}

Which produces output:

LIGHTS TURNED ON
  ...lights already on
  ...lights already on
END of program.
Deleter called.
LIGHTS TURNED OFF

whereas the output I want is:

LIGHTS TURNED ON
  ...lights already on
  ...lights already on
Deleter called.
LIGHTS TURNED OFF
END of program.

That is, I want the LIGHTS TURNED OFF after the sp3.reset() call, not when the room object leaves scope at the end of main(). For this approach, I need a clever way to NOT have an extra reference count from inside the room class (held in its resource member), but rather have only the smart pointers returned from Enter() and stored in sp1, sp2, sp3 contribute to the reference count. Yet the factory still has to be able to make copies of the same shared_ptr each time Enter() is invoked.

(Note, I realize the usage in main() doesn't show any testing of threading, I've removed that for simplicity and because the main issue with the number of shared_ptr references can be demonstrated with a single thread as shown. I believe the mutex lock in the Deleter guards against the lights getting turned off at the same time they are getting turned on by a concurrent thread.)


Solution

  • This is exactly the sort of thing that std::weak_ptr is designed for. Instead of having Room hold a std::shared_ptr have it hold a std::weak_ptr and lock() it before testing if it's valid:

    template <class R>
    class Room {
    public:
        std::shared_ptr<R> Enter() {   // factory method
            std::lock_guard<std::mutex> lock(resourceMutex);
            std::shared_ptr<R> ptr = resource.lock();
            if (!ptr) {
                ptr = std::shared_ptr<R>(new R, [this](R* ptr){ Deleter(ptr); });
                resource = ptr;
            }
            else {
                std::cout << "  ...lights already on\n";
            }
    
            // Copy the shared_ptr (increase the reference count)
            return ptr;
        }
    
    private:
        std::mutex resourceMutex;
        std::weak_ptr<R> resource;
    
        void Deleter(R* ptr){
            std::lock_guard<std::mutex> lock(resourceMutex);
            std::cout << "Deleter called.\n";
            delete ptr;
        }
    };
    

    Demo


    Note that I don't believe your custom deleter is necessary as long as it's OK for a new instance of Lights to be created before the previous one is fully destroyed. std::shared_ptr's control block is atomic: if the use count drops to 0 resource.lock() will immediately reflect the fact that resource is expired, so Room will create a new instance of Lights, even if the thread on which the last shared_ptr sharing its ownership was reset/destroyed is still executing the default deleter.