从多线程获取单例实例的安全方法

时间:2011-07-08 05:37:58

标签: c++ multithreading

方法1

DataCenter* DataCenter::getInstance()
{
    static DataCenter instance;
    return &instance;    
}

方法2

DataCenter* DataCenter::getInstance()
{
    if (!m_instanceFlag)
    {
        m_instance = new DataCenter();
        m_instanceFlag = true;
    }    
    return m_instance;      
}

我正在进行多线程编程,DataCenter将由多个线程访问。我曾经有方法2来获取DataCenter的实例,它工作正常。但我注意到我需要防止单线程实例被多线程调用。

我的问题首先是否真的需要保护单例实例?或OS是否为我这样做?第二个问题是,第一种方法是获得单例实例的正确方法吗?

提前致谢...

6 个答案:

答案 0 :(得分:5)

1.你需要保护它,即使你没有,当然,操作系统也不会为你做。使用以下代码进行线程安全:

DataCenter* DataCenter::getInstance()
{
    MutexLocker locker(DataCenter::m_mutex);
    if(!m_instanceFlag)
    {
        m_instance = new DataCenter();
        m_instanceFlag = true;
    }
    return m_instance;
}

修改

其中MutexLocker是这样的:

class MutexLocker
{
    pthread_mutex_t &mutex;
    public:
    MutexLocker(pthread_mutex_t &mutex):mutex(mutex)
    {
        if(pthread_mutex_lock(&this->mutex)!=0)
            throw std::runtime_error("mutex locking filed");
    }
    ~MutexLocker(void)
    {
        if(pthread_mutex_unlock(&this->mutex)!=0)
            throw std::runtime_error("mutex unlocking filed");
    }
}

2.第一种方法看起来不错,但不是线程安全的。

答案 1 :(得分:2)

我想我也可以提供我的答案......

首先,“方法1”在C ++ 0x上正常工作。从draft standard,第6.7(4)节(强调我的):

  

在执行任何其他初始化之前,将执行具有静态存储持续时间(3.7.1)或线程存储持续时间(3.7.2)的所有块范围变量的零初始化(8.5)。 ...否则在第一次控制通过其声明时初始化这样的变量;这样的变量在初始化完成后被认为是初始化的。 ... 如果控件在初始化变量时同时进入声明,则并发执行应等待初始化完成。

因此,如果您有C ++ 0x,“方法1”是一种简单,正确且100%可移植的方式来实现线程安全的单例。 (甚至在C ++ 0x之前,Unix上的g ++确保这个习惯用法。我不知道MSVC。)这也很可能是最快的解决方案,因为编译器可以准确地看到你在做什么,它知道比你更了解你的CPU架构。

修复“方法2”的简单方法是在整个函数周围放置一个互斥体,正如Mihran建议的那样。但这可能会带来不幸的性能影响,因此人们总是在寻找优化它的方法。大多数方法都会引入微妙的错误......

“双重检查锁定模式”是典型的例子。它看起来像这样:

if (m_instance == NULL) {
    grab_mutex();
    if (m_instance == NULL)
        m_instance = new Whatsit();
    release_mutex();
}

return m_instance;

此模式存在两个问题。首先,一般来说,单个内存访问不能保证 atomic ;不同线程同时加载和存储单个内存位置可能导致垃圾被读取。 (我认为这不太可能是一个指针值 - 当然它不会发生在x86 / x86_64上 - 但你真的希望你的代码只在一个平台上工作一天吗?)

其次,编译器和CPU都可以自由重新排序内存访问。因此,运行Whatsit构造函数然后填充m_instance的线程实际上可能无序地执行这些写入...意味着另一个线程可以测试m_instance,将其视为非-NULL,然后在初始化之前访问该对象。这假设;它确实发生在现代CPU上。更糟糕的是,CPU越现代化,就越有可能出现问题,因为CPU在重新排序内存访问方面越来越积极。

要修复此模式,m_instance的第一次读取需要具有“获取语义”,而对m_instance的写入需要具有“释放语义”。定义:如果内存加载L具有“获取语义”,则后续加载可能不会在L之前重新排序。同样,如果商店S具有“释放语义”,则 previous < / em>商店不得在S之后重新订购。

这两个都是必需的,以便“双重检查锁定”是线程安全的。 (另外,各个装载和存储必须是原子的。)

Justin指出,声明m_instance "volatile"将在MSVC上提供这些语义。但这并不是任何标准所保证的 ,事实上对于许多编译器来说并非如此(例如Unix上的g ++)。但是如果你确定你永远不会关心x86 Windows以外的任何平台 - 你不是 - 那么“volatile”就可以了。

如何使用InterlockedCompareExchangePointer之类的比较和交换操作?

if (m_instance == NULL) {
    Whatsit *p = new Whatsit();
    if (InterlockedCompareExchangePointer(&m_instance, p, NULL) != NULL)
        delete p;
}

return m_instance;

文档说InterlockedCompareExchangePointer提供了一个“完整的内存屏障”,这意味着它肯定具有发布语义。这是正确的吗?

不,不是一般的。问题是m_instance的外部读取必然具有“获取语义”。所以原则上,另一个线程在初始化之前仍然可以读取对象的成员。现代CPU 执行“投机负载”;也就是说,他们试图“猜测”你需要什么,以便提前从内存中加载它。当然,在像x86这样的高速缓存一致的平台上,CPU在指针被计算之前必须要近乎精神才能读取对象...但是如果你使用这种模式只是因为它恰好在你的系统上工作了当它在未来的某个系统中出现故障并引入夜间不可能调试失败时,有人会诅咒你的名字。所以请不要这样做。

最后,您可以使用专为一次性初始化设计的API,例如Windows上的"One-Time Initialization API"或Unix上的pthread_once。根据定义,这些工作正常,但显然它们是特定于平台的。

底线:如果您的系统为此目的支持,请使用“方法1”。

答案 2 :(得分:1)

正如人们在评论中提到的,双重检查锁不是线程安全的解决方案。您确实需要使用某种机制来序列化对资源的访问。互锁交换可能是最简单的方式之一。

template <typename T>
class Singleton
{
  private:
    Singleton();
    ~Singleton();
    Singleton& operator=(const Singleton& item);

  protected:
    static volatile long m_locker;

    static T* GetPointer()
    {
      T* pTmp = NULL;
      try
      {
         static T var;
         pTmp = &var;
      }
      catch(...)
      {
         //assert(false);
         pTmp = NULL;
      }
      return pTmp;
    }

  public:
    static T* Get()
    {
       while(::InterlockedExchange(&m_locker, 1) != 0)
         ::SwitchToThread();

       T* pTmp = GetPointer();
       ::InterlockedExchange(&m_locker, 0);
       return pTmp;
    }
};

template <typename T>
  volatile long Singleton<T>::m_locker = 0;

答案 3 :(得分:1)

如果调用getSingleton也可以初始化单例,你只需要保护单例访问 - 否则,多个线程可能会尝试同时创建它。

互斥锁足以防止竞争条件,但是,对getSingleton的每次后续调用也必须获得锁定,这会降低性能。如果这是一个问题并且您可以处理额外的复杂性,Raymond Chen显示了一种可以避免锁定的方法,允许多个线程创建单例实例并确定保留哪个using interlocked operations(代码内联下方):

Widget *g_pwidCached;

Widget *GetSingletonWidget()
{
 Widget *pwid = g_pwidCached;
 if (!pwid) {
  pwid = new(nothrow) Widget();
  if (pwid) {
   Widget *pwidOld = reinterpret_cast<Widget*>
       (InterlockedCompareExchangePointerRelease(
          &reinterpret_cast<PVOID&>(g_pwidCached),
          pwid, NULL));
   if (pwidOld) {
    delete pwid; // lost the race - destroy the redundant copy
    pwid = pwidOld; // use the old one
   }
  }
 }
 return pwid;
}

当然,这是特定于Windows的,但代码可以替换为特定于平台的互锁操作,而不会改变含义。 (作为奖励:如果您在Windows上 编码,您只需使用提供的One-Time Initialization API为您完成艰苦的工作!)

请注意,单例的构造函数不能有副作用,否则会出现意外结果。 (Raymond的full blog post中有更多细节。)

答案 4 :(得分:0)

您需要使用双重检查锁定机制,但它也可能不是100%安全。

DataCenter* DataCenter::getInstance()
{
  if (m_instance == null)
  {
    // some sort of synchronization lock    //1
     {  

        if (m_instance == null)             //2
            m_instance  = new DataCenter(); //3 
    }
  }
  return m_instance ;
}

再解释一下:

线程1进入getInstance()方法。

线程1在// 1处进入同步块,因为实例为空。

线程1被线程2抢占。

线程2进入getInstance()方法。

线程2尝试在// 1处获取锁定,因为实例仍为空。但是,因为线程1持有锁,所以线程2在// 1处阻塞。

线程2被线程1抢占。

线程1执行,因为实例在// 2处仍然为空,创建一个Singleton对象并将其引用分配给实例。

线程1退出synchronized块并从getInstance()方法返回实例。

线程1被线程2抢占。

线程2在// 1获取锁定并检查实例是否为空。

因为实例是非null,所以不会创建第二个Singleton对象,并返回由线程1创建的对象。

答案 5 :(得分:0)

是的,你确实需要这样做。如果线程1检查实例标志并为线程2换出,然后线程2执行整个getInstance()方法,则线程1将继续获取另一个实例。

这是因为它已经在错误时检查了标志,并且不会因为上下文切换而重新检查它。

如果多个线程可能同时调用getInstance(),则需要保护检查操作并将标志设置为原子单元。

当然,如果在任何其他线程开始之前从主线程中调用getInstance,则可以无法获得保护。

您可能也想放弃使用单独的标志变量的想法。您可以在加载时将实例设置为NULL,并使用:

DataCenter* DataCenter::getInstance(){
    static DataCenter *m_instance = 0;
    // begin atomic unit
    if(m_instance == 0)
        m_instance = new DataCenter();
    // end atomic unit
    return m_instance;
}