【问题标题】:Singleton with multithreads多线程单例
【发布时间】:2012-08-28 05:50:18
【问题描述】:

这个问题是在一次采访中被问到的。第一部分是编写单例类:

class Singleton
{
    static Singleton *singletonInstance;
    Singleton() {}

  public:
    static Singleton* getSingletonInstance()
    {
        if(singletonInstance == null)
        {
            singletonInstance = new Singleton();
        }
        return singletonInstance;
    }
};

然后有人问我如何在多线程情况下处理这个getSingletonInstance()。我不太确定,但我修改为:

class Singleton 
{
    static Singleton *singletonInstance;
    Singleton() {}
    static mutex m_;

  public:
    static Singleton* getSingletonInstance()
    {
        m_pend();
        if(singletonInstance == null)
        {
            singletonInstance = new Singleton();
        }
        return singletonInstance;
    }

    static void releaseSingleton()
    {
        m_post();
    }
};

然后有人告诉我,虽然需要互斥锁,但挂起和发布互斥锁效率不高,因为它需要时间。并且有更好的方法来处理这种情况。

有人知道在多线程情况下处理单例类的更好更有效的方法吗?

【问题讨论】:

  • 根本不使用单例?
  • 在多线程代码中使用全局状态是解决无限麻烦的好方法。在愚蠢的情况下加倍如此。
  • @CatPlusPlus :这很简单。傻瓜。

标签: c++ multithreading synchronization singleton mutex


【解决方案1】:

如果你有 C++11,你可以将 singletonInstance 设为原子变量,然后使用双重检查锁:

if (singletonInstance == NULL) {
    lock the mutex
    if (singletonInstance == NULL) {
        singletonInstance = new Singleton;
    }
    unlock the mutex
}
return singletonInstance;

【讨论】:

  • 如果你的编译器正确实现了C++11,你完全不需要加锁。 static Foo& getSingleton(){ static Foo foo{}; return foo; } 将在并发执行下正常工作
  • @Xeo - 好点。让它成为一个答案,我会投赞成票,但有评论说这可能效率低下,这取决于编译器如何管理函数静态对象的锁(如果有一个共享锁,你可能会争用)。跨度>
  • 好吧,我已经写过一篇了,但这个问题并不是真的重复,至少在我看来:*.com/a/11711991/500104
  • 如果您要显式编写一行“解锁互斥锁”的伪代码,那么伪代码是否也应确保在new 表达式抛出时解锁它?还是可以不用说和/或假设异常导致程序终止?在一个真正的程序中你必须决定哪个,所以我想我在这里谈论的是伪代码礼仪......
  • @SteveJessop - 如果分配引发异常,您的暗示必须解锁是正确的。我只是想避免调用m_pend()m_post(),就像在原始代码中一样,因为我不知道他们做了什么。当然,在 C++11 中你会使用一个锁对象,它的析构函数解锁了互斥锁。
【解决方案2】:

您实际上应该锁定单例,而不是实例。如果实例需要锁定,则应由调用者处理(或者可能由实例本身处理,具体取决于它公开的接口类型)

更新示例代码:

#include <mutex>

class Singleton 
{
    static Singleton *singletonInstance;
    Singleton() {}
    static std::mutex m_;

  public:

    static Singleton* getSingletonInstance()
    {
        std::lock_guard<std::mutex> lock(m_);
        if(singletonInstance == nullptr)
        {
            singletonInstance = new Singleton();
        }
        return singletonInstance;
    }
}

【讨论】:

    【解决方案3】:

    在 C++11 中,保证执行线程安全初始化:

    static Singleton* getSingletonInstance()
    {
        static Singleton instance;
        return &instance;
    }
    

    在 C++03 中,一种常见的方法是使用双重检查锁定;检查标志(或指针本身)以查看对象是否可能未初始化,如果可能,则仅锁定互斥锁。这需要某种非标准的原子读取指针(或相关的布尔标志)的方式;许多实现错误地使用了普通指针或bool,无法保证一个处理器上的更改在其他处理器上可见。代码可能看起来像这样,尽管我几乎可以肯定有什么问题:

    static Singleton* getSingletonInstance()
    {
        if (!atomic_read(singletonInstance)) {
            mutex_lock lock(mutex);
            if (!atomic_read(singletonInstance)) {
                atomic_write(singletonInstance, new Singleton);
            }
        }
        return singletonInstance;
    }
    

    这很难做到,所以我建议你不要打扰。在 C++11 中,您可以使用标准原子类型和互斥锁类型,如果出于某种原因您希望保持示例的动态分配。

    请注意,我只是在谈论同步初始化,而不是对对象的同步访问(您的版本通过将互斥锁锁定在访问器中并稍后通过单独的函数释放它来提供)。如果您需要锁来安全地访问对象本身,那么您显然无法避免每次访问都加锁。

    【讨论】:

    • 如果返回 Singleton& 而不是 Singleton * 是否保证线程安全?
    • @Romain-p,提供的示例称为 Meyers 单例。线程安全是通过依赖 C++ 标准来实现的,该标准声明:“静态变量初始化应该是线程安全的”。换句话说,静态变量的线程安全初始化是一个编译器头疼的问题。
    【解决方案4】:

    如果你使用 POSIX 线程,你可以使用 pthread_once_tpthread_key_t 的东西,这样你就可以完全避免使用互斥锁。例如:

    template<class T> class ThreadSingleton : private NonCopyable {
    public:
        ThreadSingleton();
        ~ThreadSingleton();
    
        static T& instance();
    
    private:
        ThreadSingleton( const ThreadSingleton& );
        const ThreadSingleton& operator=( const ThreadSingleton& )
    
        static pthread_once_t once_;
        static pthread_key_t  key_;
    
        static void init(void);
        static void cleanUp(void*);
    };
    

    及实施:

    template<class T> pthread_once_t ThreadSingleton<T>::once_ = PTHREAD_ONCE_INIT;
    template<class T> pthread_key_t ThreadSingleton<T>::key_;
    
    template<class T>  
    T& ThreadSingleton<T>::instance()
    {
        pthread_once(&once_,init);
    
        T* value = (T*)pthread_getspecific(key_);
        if(!value)
        {   
    
            value = new T();
            pthread_setspecific(key_,value);
        }   
        return *value;
    }
    
    template<class T> void ThreadSingleton<T>::cleanUp(void* data)
    {
        delete (T*)data;
        pthread_setspecific(key_,0);
    }
    
    template<class T> void ThreadSingleton<T>::init()
    {
        pthread_key_create(&key_,cleanUp);
    }
    

    【讨论】:

    • 我认为问题在于创建一个实例,而不是每个线程一个。
    • 这不是我理解这个问题的方式。我只是又读了一遍,我认为你是对的。对不起。
    【解决方案5】:

    正如@piokuc 所建议的,您也可以在此处使用一次函数。如果你有 C++11:

    #include <mutex>
    
    static void init_singleton() {
        singletonInstance = new Singleton;
    }
    static std::once_flag singleton_flag;
    
    Singleton* getSingletonInstance() {
        std::call_once(singleton_flag, init_singleton);
        return singletonInstance;
    }
    

    而且,是的,如果new Singleton 抛出异常,这将有效。

    【讨论】:

    • 谢谢。我不知道曾经的功能。
    • @PeteBecker 这是最好的答案,完美使用std ns。
    • @PeteBecker getSingletonInstance() 是非静态的,这是单例模式的问题,您必须将其设为静态。
    • @Yogesh — 在我编写的代码中,getSingletonInstance 不是类的成员。它不是,也不应该是静态的。
    • @PeteBecker 哦,我的错,对 cpp 有偏见。