我的单例可以被多次调用

32

我已经基于C++11实现了一个单例模式。 但在某些情况下,构造函数可能会被多次调用。

该类将被编译为静态库,并由其他so库使用(超过一个so库)。 而且系统是一个多线程系统(在Android HAL级别运行)

/// .h文件:

class Logger
{
public:

    /// Return the singleton instance of Logger
    static Logger& GetInstance() {
        static Logger s_loggerSingleton;
        return s_loggerSingleton;
    }

private:

    /// Constructor
    Logger();
    /// Destructor
    ~Logger();
}

/// 这是 .cpp 文件

Logger::Logger()
{
   ALOGE("OfflineLogger create");
}

Logger::~Logger()
{

}

应该只创建一次,例如:

03-21 01:52:20.785   728  4522 E         : OfflineLogger create

然而,我可以看到它已经被创建了多次。

03-21 01:52:20.785   728  4522 E         : OfflineLogger create
03-21 01:52:20.863   728  2274 E         : OfflineLogger create
03-21 01:52:20.977   728  2273 E         : OfflineLogger create
03-21 01:52:26.370   728  4522 E         : OfflineLogger create

问题:

  1. 我的单例设计有什么问题吗?这是线程安全问题吗?

  2. 似乎我的单例在一个作用域中运行良好,但每个包含我的单例的so库将创建自己的单例,因此我的单例不再“成为单例”。这个问题是由于每次动态链接到新的so和“静态变量”变成“局部静态”引起的吗?如果是,如何解决?


5
别忘了删除复制构造函数,否则很容易创建多个对象。删除赋值操作是个好主意,虽然将对象赋值给自己可能对你并没有太大的伤害。 - Michael Veksler
1
还要记得这里没有使用同步,所以如果在多线程环境中使用,仍然容易出现竞态条件。 - Alexander
1
@Alexander 这适用于使用对象而不是创建对象。自C++11以来,函数作用域的静态变量保证可以无竞争地初始化。 - Angew is no longer proud of SO
1
@Angew 哇,真的吗?它们终于有了用处!太好听了。 - Alexander
4
这是一个好主意:不要使用单例,因为在您的环境中这是一个困难的问题,并且已知对测试和维护有问题。相反,只需设计您的代码来创建所需对象的一个实例即可。 - T.E.D.
4个回答

28
  1. 我的单例设计有问题吗?这是线程安全问题吗?

不会。按照标准,函数局部static变量的初始化是保证线程安全的。

  1. 我的单例在一个so范围内似乎工作得很好,但包含它的每个so库都会创建自己的单例,因此我的单例不再“成为单例”了。这个问题是由于每个动态链接到新的so并且“static变量”变成“局部static”引起的吗?如果是这样,如何解决?

这是正确的结论。

而不是创建一个包含单例实现的静态库,将其制作成动态库。


非常感谢您的帮助。最终我改用动态链接,现在运行良好。 - hismart
@hismart,不用谢。很高兴我能帮到你。 - R Sahu
但是为什么多次这样做会导致这样的问题呢? - Baiyan Huang
1
@BaiyanHuang,因为每个so都会有其自己的静态库副本内置其中。 - R Sahu

4

单例模式很难,特别是与共享库一起使用时。

每个共享库都有一个独立的非共享库副本。如果没有额外的注意,那么每个副本都会有一个单例对象。

为了拥有非平凡的单例对象,我必须进行以下操作:

  1. 创建一个极低级别的库来帮助实现单例模式 - 称之为 LibSingleton

  2. 创建一个单例模板,它知道单例对象的类型。它使用魔术静态变量向 LibSingleton 发送请求,包括一个尺寸、typeid(T).name() 键和类型擦除后的构造和析构函数代码。LibSingleton 返回一个引用计数的 RAII 对象。

  3. LibSingleton 使用共享互斥锁来返回先前已经构造的名称/大小匹配的对象或者构造新对象。如果它构造了一个对象,则它存储析构函数代码。

  4. 当对 LibSingleton 数据最后一个引用计数句柄消失时,LibSingleton 会运行析构函数代码并清理其无序映射中的内存。

这允许在几乎任何地方使用非常简单的单例模式。

template<class T>
class singleton {
public:
  static T& Instance() {
    static auto smart_ptr = LibSingleton::RequestInstance(
      typeid(T).name(),
      sizeof(T),
      [](void* ptr){ return ::new( ptr ) T{}; },
      [](void* ptr){ static_cast<T*>(ptr)->~T(); }
    );
    if (!smart_ptr)
      exit(-1); // or throw something
    return *static_cast<T*>(smart_ptr.get());
  }
protected:
  singleton() = default;
  ~singleton() = default;
private:
  singleton(singleton&&) = delete;
  singleton& operator=(singleton&&) = delete;
};

使用方式如下:

struct Logger : LibSingleton::singleton<Logger> {
  friend class LibSingleton::singleton<Logger>;
  void do_log( char const* sting ) {}
private:
  Logger() { /* ... */ }
};

1
看起来大部分的精力都花在了获取第一个单例上,其他的相对容易添加。不过,这让人想到... - David K

2
这是一个想法:不要使用单例模式,因为在你的环境中这很困难,并且已知对于测试和维护有问题,而是设计你的代码只创建一个需要的对象。"最初的回答"

简单但真实... :) - Life School

1

静态变量应该移动到.cpp文件中。

简单的方法是在.h中仅保留getInstance()的声明,并将实现移动到.cpp文件中。


5
完全不能帮助。即使是内联函数也只能保证有一个静态集合。这里的问题在于共享库。 - Angew is no longer proud of SO
@AngewisnolongerproudofSO 我刚遇到了这个问题,将GetInstance的定义移动到cpp文件中对我有用。我怀疑这可以防止每个库都保留自己的副本,通过将所有权移交给包含实现的库。我不确定这有多可移植; 只是指出在Windows上使用MinGW进行测试时它有效。 - LINEMAN78

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接