Implement the C++ boost library using the singleton pattern from the design pattern

  • 2020-05-09 18:52:24
  • OfStack

Thread-safe singleton mode
1. Lazy mode: that is, a new instance of this class is generated when the first instance of this class is called, and only this instance is returned later.

Locks are needed to make them thread-safe: reason: multiple threads may enter an if statement that determines if an instance already exists, thereby non thread safety.

Use double-check to guarantee thread safety. However, it is when processing large amounts of data that the lock becomes a serious performance bottleneck.

1. Lazy mode of static member instance:


class Singleton
{
private:
  static Singleton* m_instance;
  Singleton(){}
public:
  static Singleton* getInstance();
};
 
Singleton* Singleton::getInstance()
{
  if(NULL == m_instance)
  {
    Lock();
// Borrow other classes to implement, such as boost
    if(NULL == m_instance)
    {
      m_instance = new Singleton;
    }
    UnLock();
  }
  return m_instance;
}

2. Lazy mode of internal static instance

The important thing to note here is that after C++0X, the compiler is required to guarantee the thread-safety of internal static variables, which can be left unlocked. But before C++ 0X, it still needs to be locked.


class SingletonInside
{
private:
  SingletonInside(){}
public:
  static SingletonInside* getInstance()
  {
    Lock(); 
// not needed after C++0x
    static SingletonInside instance;
    UnLock(); 
// not needed after C++0x
    return instance; 
  }
};

2. Hungry Chinese mode: that is, whether an instance of this class is called or not, an instance of this class will be generated at the beginning of the program, and only this instance will be returned later.

Thread safety is guaranteed by a static initialization instance, WHY? Because the static instance initialization is done in a single thread by the main thread before it enters the main function at the beginning of the program, there is no need to worry about multithreading.

Therefore, when performance requirements are high, this mode should be used to avoid frequent lock contention.


class SingletonStatic
{
private:
  static const SingletonStatic* m_instance;
  SingletonStatic(){}
public:
  static const SingletonStatic* getInstance()
  {
    return m_instance;
  }
};
 
// External initialization  before invoke main
const SingletonStatic* SingletonStatic::m_instance = new SingletonStatic;


An example implementation of the boost library

Singletons are a very simple pattern and should be easy to implement, but the simple implementation of C++ singletons will have some holes in it. With the thread-safe foundation above, let's take a look at how to avoid these holes by gradually evolving to the implementation of boost library.

Plan 1


class QMManager
{
public:
  static QMManager &instance()
  {
    static QMManager instance_;
    return instance_;
  }
}

This is the simplest version, and is fine with a single thread (or C++0X), but not with multiple threads, because static QMManager instance_; This sentence is not thread safe.
Static variables in local scope at compile time, the compiler creates an additional variable to indicate whether the static variable has been initialized, which is changed by the compiler as follows (pseudocode) :


static QMManager &instance()
{
  static bool constructed = false;
  static uninitialized QMManager instance_;
  if (!constructed) {
    constructed = true;
    new(&s) QMManager; //construct it
  }
  return instance_;
}

When two threads call instance() at the same time, one thread runs to if and has not set constructed value. At this point, it switches to another thread, constructed value is still false, and it also initializes the variable in if statement. Both threads execute the initialization of the singleton class, so it is no longer a singleton.

Scheme 2
One solution is to lock:


static QMManager &instance()
{
  Lock(); // Lock itself implementation 
  static QMManager instance_;
  UnLock();
  return instance_;
}

But each call to instance() would be unlocked with a lock, at a slightly higher cost.

Plan 3
If you change it by 1, make the internal static instance a static member of the class, and initialize it externally, that is, initialize the instance before include files and main functions are executed, then there will be no thread reentry problem:


class QMManager
{
protected:
  static QMManager instance_;
  QMManager();
  ~QMManager(){};
public:
  static QMManager *instance()
  {
    return &instance_;
  }
  void do_something();
};
QMManager QMManager::instance_; // External initialization 

This is known as the hungriness mode, and program 1 is initialized as soon as it is loaded, whether or not it is called.
This looks fine, but there is a problem with one 2B: calling a method of another singleton class in the constructor of this singleton class might have a problem.
See the examples:


//.h
class QMManager
{
protected:
  static QMManager instance_;
  QMManager();
  ~QMManager(){};
public:
  static QMManager *instance()
  {
    return &instance_;
  }
};
 
class QMSqlite
{
protected:
  static QMSqlite instance_;
  QMSqlite();
  ~QMSqlite(){};
public:
  static QMSqlite *instance()
  {
    return &instance_;
  }
  void do_something();
};
 
QMManager QMManager::instance_;
QMSqlite QMSqlite::instance_;

//.cpp
QMManager::QMManager()
{
  printf("QMManager constructor\n");
  QMSqlite::instance()->do_something();
}
 
QMSqlite::QMSqlite()
{
  printf("QMSqlite constructor\n");
}
void QMSqlite::do_something()
{
  printf("QMSqlite do_something\n");
}

Here QMManager's constructor calls QMSqlite's instance function, but QMSqlite::instance_ may not be initialized yet.
Here's how it works: after the program starts, before main, it goes to QMManager QMManager::instance_; This code initializes the instance_ static variable in QMManager, calls the QMManager constructor, calls QMSqlite::instance() in the constructor, and takes the instance_ static variable in QMSqlite, but before QMSqlite::instance_ is initialized, the problem arises.
The static data area space should be allocated first. Before calling the QMManager constructor, the QMSqlite member function already exists in memory, but it has not been tuned to its constructor, so the output is like this:


QMManager constructor
QMSqlite do_something
QMSqlite constructor

Plan 4
Then how to solve this problem? Singleton object as static local variable has thread safety problem, as class static global variable is initialized at the beginning of 1, has the above 2B problem, so combining the above two ways, can solve these two problems. The way boost is implemented is that the singleton is a static local variable, but an auxiliary class is added so that the singleton can be initialized at the beginning of 1. As follows:


//.h
class QMManager
{
protected:
  struct object_creator
  {
    object_creator()
    {
      QMManager::instance();
    }
    inline void do_nothing() const {}
  };
  static object_creator create_object_;
 
  QMManager();
  ~QMManager(){};
public:
  static QMManager *instance()
  {
    static QMManager instance;
    return &instance;
  }
};
QMManager::object_creator QMManager::create_object_;
 
class QMSqlite
{
protected:
  QMSqlite();
  ~QMSqlite(){};
  struct object_creator
  {
    object_creator()
    {
      QMSqlite::instance();
    }
    inline void do_nothing() const {}
  };
  static object_creator create_object_;
public:
  static QMSqlite *instance()
  {
    static QMSqlite instance;
    return &instance;
  }
  void do_something();
};
 
QMManager::object_creator QMManager::create_object_;
QMSqlite::object_creator QMSqlite::create_object_;

Combined with scenario 3's.cpp, you can now see the correct output and call:


class SingletonInside
{
private:
  SingletonInside(){}
public:
  static SingletonInside* getInstance()
  {
    Lock(); 
// not needed after C++0x
    static SingletonInside instance;
    UnLock(); 
// not needed after C++0x
    return instance; 
  }
};
0

Take a look at the execution process here:
Initializes the QMManager class global static variable create_object_
- > Call the constructor of object_creator
- > Call the QMManager::instance() method to initialize the singleton
- > Execute the constructor for QMManager
- > Call QMSqlite: : instance ()
- > Initializes the local static variable QMSqlite instance
- > Execute the constructor of QMSqlite and return the singleton.
The difference from scheme 3 is that when QMManager calls QMSqlite singleton, scheme 3 takes a global static variable, which is not initialized, while the singleton of scheme 4 is a static local variable, which is initialized.
The difference from the original scenario 1 is that the singleton is initialized before the main function and there are no thread-safety issues.

Eventually boost
For the sake of clarity, remove the template above, the actual use is to use the template, do not have to write so much repetitive code, this is the boost library template implementation:


class SingletonInside
{
private:
  SingletonInside(){}
public:
  static SingletonInside* getInstance()
  {
    Lock(); 
// not needed after C++0x
    static SingletonInside instance;
    UnLock(); 
// not needed after C++0x
    return instance; 
  }
};
1

In fact, the implementation of the Boost library is like a few patches, using some strange tricks, although it does bypass the pit to achieve the requirements, but it feels pretty bad.


Related articles: