首页 > Note > 从单例模式谈起(一)

从单例模式谈起(一)

2019年4月8日 发表评论 阅读评论

单例模式可能是大家最为熟知的一种设计模式,本身没什么好谈的。但是在 C++ 中,由单例模式可以引出一系列的问题,可能会比较有意思,这里探讨一下。

常见的简单实现

1.使用 static 实现

class S1 {
    public:
        static S1& GetInstance() {
            static S1 s;
            return s;
        }   
        void fun() {}

    private:
        S1(){cout<<"S1 inited"<<endl;}
        S1(const S1&) = delete;
        S1& operator=(const S1&) = delete;
};

class S2{ 
    public:
        static S2& GetInstance(){
            return s;
        }   
        void fun(){}
    private:
        S2(){cout<<"S2 inited"<<endl;}
        //...
    private:
        static S2 s;
};
S2 S2::s;

其中 S2 是要避免的。因为 1. 类的静态成员变量的初始化时间一般早于 main 函数 2. 如果静态成员的初始化里调用了其它类,可能出现未定义的错误。

2.使用指针判断是否初始化

class S3{
    public:
        static S3& GetInstance(){
            if(s == 0)
                s = new S3();
                 return *s;
        }

    private:
        S3(){}
        static S3 *s;
};

这两种方式在单线程程序中使用都是可以的,但如果在多线程中,就会出现问题。

Magic Static

对于 S1 要注意, C++ 局部静态变量的初始化可能不是线程安全的 , 这就是 Magic Static ,是指 返回一个静态局部变量的引用 的用法,在某些情况下,如S1 可能会被编译器这样解析:

//伪代码
static S1& GetInstance(){
    static bool constructed = false;
    static uninited S1 s;
    if(!constructed){
        constructed = true;
        new (&s)S1;
    }
    return s;
}

这不是线程安全的。这里的 “某些情况” 是指 C++11之前的编译器。尽管C++11 标准规定这个过程应该是线程安全的,但是在 VS2015 之前,微软还是未实现这个标准。参见这里
Thread-Safe “Magic” Statics Static local variables are now initialized in a thread-safe way, eliminating the need for manual synchronization. Only initialization is thread-safe, use of static local variables by multiple threads must still be manually synchronized. The thread-safe statics feature can be disabled by using the /Zc:threadSafeInit- flag to avoid taking a dependency on the CRT. (C++11)
以及这里这里

Double-Checked Locking Problem

对于 S3, 在多线程中, “s==0” 这个条件可能会被多个线程判断为 true, 这会导致 s 被多次初始化, 而只有最后一次初始化有效, 那么对其改进如下:

static S3& GetInstance(){
    Lock lock; 
    if(s == 0)
        s = new S3();
    return *s; 
}

这是线程安全的,但引出的问题是,每次都会调用该方法都要加解一次锁,加解锁的过程是很耗资源的。那么对于上面的代码可以有如下改进:

static S3& GetInstance(){
    if(s == 0){
        Lock lock;
        if(s == 0)
            s = new S3();
    }
    return *s; 
}

这段代码解决了每次调用都要加解锁问题。但它仍然不是线程安全的。编译器可能会将代码解析成这样:

static S3& GetInstance(){
    if(s == 0){ 
        Lock lock;  
        if(s == 0){ 
            S3* tmp = operator new(sizeof(S3));
            tmp = new (s) S3;
            s = tmp;
        }                                                                       
    }                                                                               
    return *s;                                                                          
}

但也可能解析成这样:

static S3& GetInstance(){
    if(s == 0){
        Lock lock;
        if(s == 0){
            s = operator new(sizeof(S3)); 
            new (s) S3;  
        }
    }
    return *s; 
}

对于后一种情况,它将先为 s 分配一段内存,再将为它构建对象: 它不是线程安全的。

如图,当线程1为 s分配内存后,线程2 判断 s==0 为flase, 此时线程2 将返回一个已分配内存但未初始化的指针, 可能会引起程序崩溃。

volatile

在网上有很多帖子认为使用 volatile 可以解决 DCLP , 实际情况是, 使用 volatile 无法完美解决这个问题。这个我们下一节再作讨论。

基于 C++11 的解决方案

C++11 为我们提供了很多方便好用的同步原语,可以帮助我们解决前面提到的问题。

atomic

将变量声明为原子的,可以确保双检锁正确执行:

class S3 {
 public:
  static S3& GetInstance() {
    if (s == 0) {
      std::lock_guard<std::mutex> locker(mu);
      if (s == 0) {
        s = new S3();
      }
    }
    return *s;
  }

  void fun() { cout << "aa" << endl;
  }

 private:
  S3(){};
  static std::mutex mu;
  static std::atomic<S3*> s;
};

std::mutex S3::mu;
std::atomic<S3*> S3::s = 0;

callonce

使用 callonce /onceflag 则是一种更优雅的方式:

class S3 {
 public:
  static S3& GetInstance() {
    std::call_once(flag_, []() { s = new S3(); });
    return *s;
  }

  void fun() {}

 private:
  S3() = default;

 private:
  static S3* s;
  static std::once_flag flag_;
};

S3* S3::s;
std::once_flag S3::flag_;

使用模板的单例模式

更多的时候, 我们是仅定义一个单例模式的基类, 使需要使用单例的类来继承这个基类。定义如下:

template <class T>
class SingletonBase {
 public:
  static T* GetInstance() {
    std::call_once(flag_, [&]() { ins_.reset(new T); });
    return ins_.get();
  }

  virtual ~SingletonBase<T>() = default;

 protected:
  SingletonBase<T>() = default;
  SingletonBase<T>(const SingletonBase<T>&) = delete;
  SingletonBase<T>& operator=(const SingletonBase<T>&) = delete;

 protected:
  static std::unique_ptr<T> ins_;
  static std::once_flag flag_;
};

template <class T>
std::unique_ptr<T> SingletonBase<T>::ins_;

template <class T>
std::once_flag SingletonBase<T>::flag_;

class S3 : public SingletonBase<S3> {
  friend class SingletonBase<S3>;

 public:
  void fun() {}
  ~S3() = default;

 private:
  S3() {}
};

多参数构造函数的单例模式

template <class T>
class SingletonBase {
 public:
  template <class... Args>
  static T* GetInstance(Args&&... args) {
    std::call_once(flag_, [&]() { ins_.reset(new T(std::forward<Args...>((args)...))); });
    return ins_.get();
  }

  static T* GetInstance() {
    if (!ins_) throw std::logic_error("please init class first");
    return ins_.get();
  }

  virtual ~SingletonBase<T>() = default;

 protected:
  SingletonBase<T>() = default;
  SingletonBase<T>(const SingletonBase<T>&) = delete;
  SingletonBase<T>& operator=(const SingletonBase<T>&) = delete;

 protected:
  static std::unique_ptr<T> ins_;
  static std::once_flag flag_;
};

template <class T>
std::unique_ptr<T> SingletonBase<T>::ins_;

template <class T>
std::once_flag SingletonBase<T>::flag_;


//BAD inherit
class S3 : public SingletonBase<S3> {
  friend class SingletonBase<S3>;

 public:
  void fun() {}
  ~S3() = default;

 private:
  S3() {}
};

class S4 : public SingletonBase<S4> {
  friend class SingletonBase<S4>;
 public:
  void fun() {}

 private:
  S4(const int&& n) : size_(n) {}

 private:
  int size_;
};

int main() {
  S3::GetInstance()->fun(); //BAD inherit
  S4::GetInstance(1)->fun();
  S4::GetInstance()->fun();
  return 0;
}

此时必须注意,需要和无参数的单例模式分开来。