简体   繁体   中英

Double-checked creation of thread-safe singleton and lock-free

I wrote the following code creating singleton instance of my interface manager.

#include <intrin.h>
#pragma intrinsic(_ReadWriteBarrier)

boost::mutex global_interface_manager_creation_mutex;
interface_manager* global_interface_manager = NULL;

interface_manager* get_global_interface_manager() {
    interface_manager* volatile temp = global_interface_manager;
    _ReadWriteBarrier();
    if (temp == NULL) {
        boost::mutex::scoped_lock(global_interface_manager_creation_mutex);

        temp = global_interface_manager;

        if (temp == NULL) {
            temp = new interface_manager();
            _ReadWriteBarrier();
            global_interface_manager = temp; 
        }
    }

    return temp;
}

But I don't want to use the lock and memory barrier so change the code to:

interface_manager* get_global_interface_manager() {
    interface_manager* volatile temp = global_interface_manager;

    __assume(temp != NULL);
    if (temp == NULL) {
        temp = new interface_manager();
        if(NULL != ::InterlockedCompareExchangePointer((volatile PVOID *)&global_interface_manager, temp, NULL)) {
            delete temp;

            temp = global_interface_manager;
        }
    }

    return temp;
}

It seems like this code works well but I don't be sure and I really don't know how to test it is correct.

My question would be: Is it really, really, really necessary to make a threadsafe singleton?

Singletons are debatable, but they do have their uses (and I guess discussing those would be going far off-topic).

However, threadsafe singletons are something that is 99.99% of the time unnecessary and 99.99% of the time implemented wrong, too (even people who should know how to do it right have proven in the past that they got it wrong). So, I think that in this case "do you really need this" is a valid concern.

If you create an instance of your singleton at application startup, for example from within main(), there will be only one thread. That can be as easy as calling get_global_interface_manager() once, or calling yourlibrary::init() which implicitly calls get().

Any concerns about thread-safety are irrelevant as soon as you do this, as there will be forcibly only one thread at this time. And, you are guaranteed that it will work. No ifs and whens.

A lot of, if not all, libraries require you to call an init function at startup, so that is not an uncommon requirement, either.

Have you looked into using pthread_once ? http://sourceware.org/pthreads-win32/manual/pthread_once.html

This is the use case it was made for.

#include <stddef.h> // NULL
#include <pthread.h>

static pthread_once_t once_control = PTHREAD_ONCE_INIT;
static interface_manager* m;

static void* init_interface_manager()
{
    m = new interface_manager;
    return NULL;
}

interface_manager* get_global_interface_manager()
{
    pthread_once(&once_control, &init_interface_manager);
    return m;
}

You could store the singleton as an atomic reference. After instantiating, CAS to set the reference. This will not guarantee that two copies don't get instantiated, only that the same one will always be returned from inst. Since standard C doesn't have atomic instructions I can show it in Java:

class Foo
{
    static AtomicReference<Foo> foo = new AtomicReference<>();

    public static Foo inst()
    {
        Foo atomic = foo.get();
        if(atomic != null)
            return atomic;
        else
        {
            Foo newFoo = new Foo();
            //newFoo will only be set if no other thread has set it
            foo.compareAndSet(null, newFoo);
            //if the above CAS failed, foo.get would be a different object
            return foo.get();
        }
    }
}

One of the difficult parts of multi-threaded programming is that something might appear to work 99.9% of the time, then fail miserably.

In your case, there's nothing to prevent two threads from getting NULL back from the global pointer and both allocating new singletons. One will get deleted, but you'll still pass it back as the return value from the function.

I even had trouble convincing myself that my own analysis was correct.

You could fix it easily by returning global_interface_manager rather than temp . There's still the possibility of creating an interface_manager that you turn around and delete, but I assume that was your intention.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM