7
\$\begingroup\$

I actually feel bad posting "yet another singleton"... I wrote the following one many years ago and had recently found another application for it. We had many threads, each running the same function that requires the use of a boost::asio::io_service instance. It was best that all threads shared the same io_service instance and also that that instance be destroyed before main returned. That last requirement (whose validity I now question, but, whatever..) meant no global or static object.

Here's a function that returns an instance of an object wrapped in a shared_ptr. What's special about it though is that we keep an additional reference count so the singleton is eagerly destructed when the last shared_ptr is gone.

For example, if you call the function twice, you have two shared_ptrs, each with a ref-count of 1, but our singleton's ref-count is 2. On the other hand, if you call the function only once and copy the returned shared_ptr, the shared_ptr has a ref-count of 2 but our singleton's ref-count is at one. Either way, the singleton instance gets destructed when the last shared_ptr is destructed.

One disadvantage of this code is that the class is instantiated from its default constructor.

#include <memory>
#include <mutex>

// Deleter function given to the shared_ptr returned by get_shared_singleton.
template<typename T>
void release_shared(std::mutex& m, int& n, T*& p)
{
    std::lock_guard<std::mutex> lg(m);
    if(!--n)
    {
        delete p;
        p = 0;
    }
}

template<typename T>
std::shared_ptr<T> get_shared_singleton()
{
    static std::mutex m;

    std::lock_guard<std::mutex> lg(m);

    static int n = 0;   // Ref count.
    static T* p = 0;    

    if(!p) p = new T();
    ++n;

    return std::shared_ptr<T>(p, std::bind(release_shared<T>, std::ref(m), std::ref(n), std::ref(p)));
}

The requirements, more clearly stated:

  1. You wrote a function foo() that requires an instance of object X to perform its duty.
  2. foo() can be invoked concurrently from different threads.
  3. Concurrent foo()s must share the same instance of object X.
  4. If no foo() is running, there must be no instance of object X alive.
  5. foo() is in a library, you have no control of main() or the lifetime of threads that invoke foo().

What is the best code to provide a shared instance of an object that is both lazily-constructed and eagerly-destructed?

\$\endgroup\$
  • \$\begingroup\$ Konrad mentioned that someone in the C++ chat had some other solution. That was me: gist.github.com/3284629. However that solution doesn't allow for reinitialization, which you seem to desire. To be honest, the way it is now, this looks like a nice question for StackOverflow instead. \$\endgroup\$ – R. Martinho Fernandes Aug 8 '12 at 11:31
  • \$\begingroup\$ Why do you need item #4? It seems to me like this is both the most difficult requirement and a quite large WTF. \$\endgroup\$ – DeadMG Aug 9 '12 at 9:51
  • \$\begingroup\$ Well... In effect, 4 follows from 5. If you can't "manually" destroy X, how else, other than a "self-destructing" X, could you achieve the effect? \$\endgroup\$ – screwnut Aug 13 '12 at 17:28
7
\$\begingroup\$

You can use a static weak_ptr in the function to cache the shared instance without extending its lifetime:

template<typename T>
std::shared_ptr<T> get_shared_instance()
{
    static std::mutex m;
    static std::weak_ptr<T> cache;

    std::lock_guard<std::mutex> lg(m);

    std::shared_ptr<T> shared = cache.lock();
    if (cache.expired()) {
        shared.reset(new T);
        cache = shared;
    }
    return shared;
}

So, the first caller will find a default-initialized weak_ptr counts as expired, and construct a new T.

While the first caller is active (and keeping its new T alive), concurrent callers will get the same object, cached in the weak_ptr. When the last concurrent user is done, the object will be destroyed.

A subsequent caller will find the weak_ptr expired (same as before the first call), and create a new T ... etc. etc.

\$\endgroup\$
3
\$\begingroup\$

Well, the first objection to this code is that it’s not actually a singleton. It provides a centralised way of acquiring a a shared instance, true, but it doesn’t ensure that this is the only way an instance can be retrieved.

On the contrary – the code requires that the class is publicly default constructible, i.e. not a singleton.

Next, to address your actual use-case, the easiest and cleanest method is having a local object in the function that dispatches the threads, which is passed to the individual threads. Pseudo-code (since I never worked with Boost.Asio):

void f() {
    some_type the_shared_object;

    for (int i = 0; i < 5; ++i)
        boost::asio::thread_pool.spawn(some_thread_entry_point, the_shared_object);

    boost::asio::thread_pool.wait_all();
}

Of course, this solution might need to be adapted but the general pattern remains valid.

Coming back to your code, there are at least three things (apart from the name which, as I said, is wrong) which I’d improve:

  • Use an unsigned int to count instances. int makes no sense.

  • Use finer-grained locking (double-checked locking) for better performance. At the moment, your code uses a lot of redundant, costly locks.

  • Worst of all, the code comes directly from the redundant ministry of redundancy: shared_ptr provides (thread-safe!) reference counting for you. Your code essentially provides functionality that shared_ptr already has. The following code does essentially the same:

    template<typename T>
    std::shared_ptr<T> get_shared_instance()
    {
        static std::shared_ptr<T> p(new T());
        return p;
    }
    

    Notice that this code uses lazy, thread-safe initialisation. However, the resulting object may be destructed later than you might want. If you absolutely require this, you can reset the shared_ptr at the end of main – this can of course be done automatically by making the above a member function of a class which has p as a member variable and is instantiated locally in main.

\$\endgroup\$
  • \$\begingroup\$ Thanks for your input. But now I have to add one more detail. This code is part of a library. As such, I have no control over when the threads are created and I also have no control over main(). Does that change the perspective somewhat? \$\endgroup\$ – screwnut Aug 7 '12 at 17:58
  • \$\begingroup\$ Also, I don't understand how the simple solution you propose solves the problem I'm trying to avoid. The static shared_ptr<> will be destructed after main() returns. You can't reset() the static variable p. All you can reset() is a copy of it and the ref-count of p is always at least 1. If you can fix that by wrapping it in a class, would you be kind enough to write that sample code? \$\endgroup\$ – screwnut Aug 7 '12 at 17:59
  • \$\begingroup\$ To address the first point: it might change it – that depends on whether the library abstracts this in a clean way. You don’t actually need control over thread creation – but you do need control over the joining of (waiting for) the threads. If you can wait for all threads to finish, you can probably use a local variable and somehow pass it to all the threads. \$\endgroup\$ – Konrad Rudolph Aug 7 '12 at 19:23
  • \$\begingroup\$ About the second point, the code in my answer is the result of some iterating back and forth in my head and the result no longer perfectly approaches what you want. But you can call reset on a static object (however, to do this you need to expose it so you can no longer use a local static variable. Somebody in the C++ chat actually had a better (albeit a bit hacky) solution for this. Let’s hope he’ll still post it here. \$\endgroup\$ – Konrad Rudolph Aug 7 '12 at 19:25
  • \$\begingroup\$ Well, actually, I don't even have control over the threads' lifetime. My library provides a function foo(). That function can be invoked by 10 threads in parallel or just twice with 30 minutes in between. And I need is for all concurrent foo()s to share the same instance of object X and if no foo() is running then there must be no object X instance. (I think I'm going to edit my question to add this more generically stated requirements). \$\endgroup\$ – screwnut Aug 8 '12 at 1:28

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.