单例模式

三个要点:

  • 提供对唯一实例的全局访问点
  • 多线程安全
  • 防止私自创建实例

单例模式有两种实现方法:分别是懒汉和饿汉模式。

  • 懒汉模式,即非常懒,不用的时候不去初始化,所以在第一次被使用时才进行初始化;

    只有在真正使用的时候才会实例化一个对象并交给自己的引用

  • 饿汉模式,即迫不及待,在程序运行时立即初始化。例模式(饿汉模式和懒汉模式),线程安全版本

实现:私有化构造函数,以防止外界创建单例类的对象;使用类的私有静态指针变量指向类的唯一实例,并用一个公有的静态方法获取该实例。

应用场景:资源共享的情况下,避免由于资源操作时导致的性能或损耗等,如日志文件,应用配置。

饿汉模式

在单例类定义时实例化,不需要用锁,就可以实现线程安全。

原因:在程序运行时就定义了对象,并对其初始化。之后,不管哪个线程调用成员函数getinstance(),都只不过是返回一个对象的指针而已。

class single{
private:
	static single* p;
	single(){}
	~single(){}

public:
	static single* getinstance();

};

single* single::p = new single();
single* single::getinstance(){
	return p;
}

优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。

缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费

懒汉模式

将实例动态内存分配过程放到实例访问点 GetInstance 中

线程不安全

单例实例被延迟加载,即只有在真正使用的时候才会实例化一个对象并交给自己的引用。

class single{
private:
	static single *p;
	single(){}
	~single(){}

public:
	static single* getinstance();
};

single* single::p = NULL;
single* single::getinstance(){
	if (nullptr == p)
		p = new single;

	return p;
}

如果在多线程下,一个线程进入了if (nullptr == p)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例,nullptr == p

单检锁

可以通过在 getInstance 方法中添加互斥锁 mutex 来解决多线程场景下的资源争抢问题,为了实现自解锁,通常使用 mutex 的 RAII(Resource Acquisition Is Initialization)包装器类 std::lock_guard

class LazySingleton
{
private:
    static LazySingleton *p;
    static std::mutex mutex_;

    LazySingleton() {}
    ~LazySingleton() {}

public:
    //公有静态方法获取实例
    static LazySingleton *getinstance();
}

LazySingleton *LazySingleton::p{nullptr};
std::mutex LazySingleton::mutex_;

LazySingleton *LazySingleton::getinstance()
{
    std::lock_guard<std::mutex> lock(mutex_);
    if (nullptr == p)
    {
        pinstance_ = new LazySingleton;
    }
    return pinstance_;
}

只有初次调用 GetInstance 时才有必要执行锁操作,实际每次调用实例都需要加锁,降低实例访问效率

双检锁

针对单检锁方法中存在的性能问题,有一种所谓的双检锁模式,即在 getInstance 中执行锁操作前,在最外层额外地进行一次实例指针的检查操作(“双检”的体现),这样可以保证实例指针完成内存分配后,单纯的实例访问操作不会再附带锁操作带来的性能开销

class LazySingleton
{
private:
    static LazySingleton *p;
    static std::mutex mutex_;

    LazySingleton() {}
    ~LazySingleton() {}

public:
    static LazySingleton *getInstance();
};

LazySingleton *LazySingleton::p{nullptr};
std::mutex LazySingleton::mutex_;

LazySingleton *LazySingleton::getInstance()
{
    if (nullptr == pinstance_)
    {
        std::lock_guard<std::mutex> lock(mutex_);
        if (nullptr == pinstance_)
        {
            pinstance_ = new LazySingleton;
        }
    }
    return p;
}

使用双重检测同步延迟加载去创建单例的做法是一个非常优秀的做法,其不但保证了单例,而且切实提高了程序运行效率

Meyers 单例

将经典实现中的私有唯一实例删掉,改为在instance函数里定义一个静态的实例,也可以保证拥有唯一实例,在返回时只需要返回其指针就可以

//C++11后安全,不需要加锁
class MeyersSingleton
{
private:
    MeyersSingleton() {}
    ~MeyersSingleton() {}

public:
    ////局部静态变量单例模式
    static MeyersSingleton *getinstance();
};

MeyersSingleton *MeyersSingleton::getinstance()
{
    static MeyersSingleton obj;
    return &obj;
}

Meyers 单例本质上也是一种懒汉式实现,但其在 C++11 及以后的标准中是天然多线程安全的

C++规定:如果多个线程同时尝试初始化相同的静态局部变量,初始化动作只会发生一次,这个内部特性通常也是通过双检锁模式实现的

apollo中单例模式

  • eg1:SensorManager

在读取传感器数据,元数据的读取是通过SensorManager来完成的,SensorManager 类经宏定义 DECLARE_SINGLETON(SensorManager) 修饰成为单例类

SensorManager 定义在 apollo/modules/perception/common/sensor_manager/sensor_manager.h

class SensorManager {
  // ...
  //
  // other code
  //
  // ...
  DECLARE_SINGLETON(SensorManager)
};
  • eg2:ConfigManager

在激光雷达障碍物检测LidarObstacleDetection的init函数中,读取激光雷达传感器的参数通过ConfigManager来完成的,ConfigManager类经宏定义DECLARE_SINGLETON(ConfigManager)修饰成为单例类

ConfigManager 定义在 apollo/modules/perception/lib/config_manager/config_manager.h

class ConfigManager {
  // ...
  //
  // other code
  //
  // ...
  DECLARE_SINGLETON(ConfigManager)
};

DECLARE_SINGLETON(classname) 定义在 apollo/cyber/common/macros.h 中:

#define DISALLOW_COPY_AND_ASSIGN(classname) \
  classname(const classname &) = delete;    \
  classname &operator=(const classname &) = delete;

#define DECLARE_SINGLETON(classname)                                            \
public:                                                                         \
  static classname *Instance(bool create_if_needed = true)                      \
  {                                                                             \
    static classname *instance = nullptr;                                       \
    if (!instance && create_if_needed)                                          \
    {                                                                           \
      static std::once_flag flag;                                               \
      std::call_once(flag, [&] { instance = new (std::nothrow) classname(); }); \
    }                                                                           \
    return instance;                                                            \
  }                                                                             \
                                                                                \
  static void CleanUp()                                                         \
  {                                                                             \
    auto instance = Instance(false);                                            \
    if (instance != nullptr)                                                    \
    {                                                                           \
      CallShutdown(instance);                                                   \
    }                                                                           \
  }                                                                             \
                                                                                \
private:                                                                        \
  classname();                                                                  \
  DISALLOW_COPY_AND_ASSIGN(classname) // 嵌套的宏定义 DISALLOW_COPY_AND_ASSIGN

Instance 方法

提供对唯一实例的全局访问点

实例的唯一性通过局部静态的实例指针实现:

static classname *instance = nullptr; 

线程安全:

对于data race,有两种解决方案:

  • 一种使用mutex来对共享数据进行保护操作,确保在同一时间下,只有一个线程可以对共享数据尽心操作
  • C++标准库还提供了一个特殊的解法,那就是使用std::once_flag和std::call_once(头文件为<mutex>)

apollo采用使用std::once_flag和std::call_once,即一种懒汉实现方式,实例指针的动态内存分配放到了访问点中

// 执行内存分配,当下次再次执行到这个语句时,就不会再去执行了,因为flag被执行过一次了

static std::once_flag flag;    
std::call_once(flag, [&] { instance = new (std::nothrow) classname(); });

std::call_once 通过间接调用 pthread_once 函数来确保传入的可调用对象即使在多线程场景下也只能被执行一次,pthread_once 的底层实现基于互斥锁和条件变量

防止私自创建实例

通过私有化默认构造函数和另一个用于禁止拷贝构造和拷贝赋值的宏定义实现

#define DISALLOW_COPY_AND_ASSIGN(classname) \
  classname(const classname &) = delete;    \
  classname &operator=(const classname &) = delete;

CleanUp 方法

CleanUp 静态方法,该方法允许用户调用时执行一些自定义的清理工作(可选):

调用 CleanUp 方法时,若发现实例指针不为空,则会调用 CallShutdown 模板函数,CallShutdown 模板函数包含两个经类型萃取(type traits)进行重载的实现:

template <typename T>
typename std::enable_if<HasShutdown<T>::value>::type CallShutdown(T *instance)
{
  instance->Shutdown();
}

template <typename T>
typename std::enable_if<!HasShutdown<T>::value>::type CallShutdown(
    T *instance)
{
  (void)instance;
}

enable_if 的主要作用就是当某个 condition 成立时, enable_if可以提供某种类型。 enable_if源码如下:

// STRUCT TEMPLATE enable_if
template <bool _Test, class _Ty = void>   //泛化版本
struct enable_if {}; // no member "type" when !_Test
 
template <class _Ty> //偏特化版本:怎么理解:只有这个偏特化版本存在,才存在一个名字叫做type的类型别名(类型)
struct enable_if<true, _Ty> { // type is _Ty for _Test
	using type = _Ty;
};

只有当第一个模板参数_Test为 true 时,type 才有定义(type即第二个模板参数_Ty);否则使用 type 会产生编译错误,且默认模板参数可以不必指定第二个参数类型。

为了理解 std::enable_if<> 的实现,我们先来了解一下 SFINAE

SFINAE 是C++ 的一种语言属性,具体内容就是”从一组重载函数中删除模板实例化无效的函数”,SFINAE 的的全称是 Substitution Failure Is Not An Error。

SFINAE看成是C++语言的一种特性或者说一种模板设计(编译器遵循的原则)中要遵循的重要原则

在对一个函数调用进行模板推导时,编译器会尝试推导所有的候选函数(重载函数,模板,但是普通函数的优先级要高),以确保得到一个最完美的匹配

也就是说在推导的过程中,如果出现了无效的模板参数,则会将该候选函数从重载决议集合中删除,只要最终得到了一个 perfect match ,编译就不会报错

// 创建类型萃取模板类 HasShutdown,HasShutdown 可检查模板类型参数 T 中是否包含 Shutdown 方法
DEFINE_TYPE_TRAIT(HasShutdown, Shutdown)

DEFINE_TYPE_TRAIT 定义在 apollo/cyber/base/macros.h 中:

#define DEFINE_TYPE_TRAIT(name, func)                     \
  template <typename T>                                   \
  struct name {                                           \
    template <typename Class>                             \
    static constexpr bool Test(decltype(&Class::func)*) { \
      return true;                                        \
    }                                                     \
    template <typename>                                   \
    static constexpr bool Test(...) {                     \
      return false;                                       \
    }                                                     \
                                                          \
    static constexpr bool value = Test<T>(nullptr);       \
  };                                                      \
                                                          \
  template <typename T>                                   \
  constexpr bool name<T>::value;

DEFINE_TYPE_TRAIT 会根据宏参数 name 创建一个同名的类型萃取模板类,并检查模板类型参数 T 中是否包含与宏参数 func 同名的方法,若包含,则模板类的 value 成员被置为 true,否则置为 false。应该注意的是,func 在 T 中必须是公有的,否则无法被发现

再回到 CallShutdown 模板函数,HasShutdown 检查实例指针所属的类类型中是否包含 Shutdown 方法,若是,则 CleanUp 调用下面模板函数:

typename std::enable_if<HasShutdown<T>::value>::type CallShutdown(T *instance)
{
  instance->Shutdown();
}

否则,执行:

typename std::enable_if<!HasShutdown<T>::value>::type CallShutdown(T *instance)
{
  // (void)instance; 没有实际含义,仅用于避免编译器生成类似“变量定义但未被使用”的警告。
  (void)instance;
}
欢迎大家关注笔者,你的关注是我持续更博的最大动力


Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐