Singleton设计模式——《现代C++设计——泛型编程与设计模式》学习笔记

1.1    Singleton(单实例)

如果我们觉得一个类只需要产生一个实例,我们就把它设计为Singleton(单实例模式)。

Singleton是在我们项目中使用频率最高的一个模式,该模式保证一个类仅有一个实例,并提供一个访问它的全局访问点

 

       Singleton是一种经过改进的全局变量,该模式的描述很简单,但实现却很复杂。特别是Singleton对象的生命周期管理是实现Singleton时最伤脑筋的地方。

  本文将讨论以下几个主题:

l  与单纯的全局对象相比,Singleton的特性

l  用以支持单实例的C++基本手法

l  资源泄漏问题与Meyers Singleton

l  Dead Reference问题

l  Phoenix Singleton

l  带寿命的Singleton

l  多线程问题

l  在动态库中使用Singleton 

1.1.1  静态数据+静态函数 = Singleton

Singleton好像可以由静态成员变量+静态成员函数来取代?

例如:

Class Font{…};

Class PrintPort{…};

Class PrintJob {…};

Class MyOnlyPrinter

{

Public:

  Static void AddPrintJob(PrintJob& newJob)

{

  If(printQueue_.empty() && printingPort_.available())

{

  printingPort_.send(newJob.Data());

}

Else

{

  printQueue_.push(newJob);

}

}

Private:

  Static queue<PrintJob> printQueue_;

  Static PrinterPort printingPort_;

  Static Font defaultFont_;

}

 

PrintJob somePrintJob(“MyDocument.txt”);

MyOnlyPrinter::AddPrintJob(somePrintJob);

 

上述代码有哪些缺点?

我认为主要缺点有两点:

n  代码的初始化和清理可能会有麻烦。

n  初始化工作在程序一启动时就已经完成,如果静态函数到程序退出时都没有调用,我们的初始化工作就白做了。

 

1.1.2  用以支持Singleton的一些C++基本手法

单实例的原始版实现代码:

class Singleton

{

public:

    static Singleton *Instance();  // 单例操作

private:

    Singleton();//关闭缺省构造函数

    Singleton(const Singleton&);//关闭拷贝构造函数

    Singleton& operator=(const Singleton&);//关闭赋值运算符

    ~Singleton();//避免被外界delete

    static Singleton *m_Instance;  // 单例指针

};

// 创建单例指针并初始化

Singleton *Singleton::m_Instance = NULL;

 

// 单例操作实现

Singleton *Singleton::Instance()

{

    if (NULL == m_Instance)

    {

        m_Instance = new Singleton();

    }

    return m_Instance;

}

 

// 构造函数

Singleton::Singleton()

{

}

 

// 析构函数

Singleton::~Singleton()

{

}

 

通过上述代码,Singleton对象的唯一性在编译期就已经实现,这正是C++实现Singleton设计模式的精髓所在。

 

除了使用New操作符动态分配内存来实现Singleton,我们还经常通过静态变量实例方式来实现这个模式,下面给出了一种用静态变量方式实现Singleton的代码。

class Singleton

{

public:

    static Singleton &Instance();    // 单例操作

private:

   Singleton();//关闭缺省构造函数

   Singleton(const Singleton&);//关闭拷贝构造函数

   Singleton& operator=(const Singleton&);//关闭赋值运算符

   ~Singleton();//避免被外界delete private:

   static Singleton m_Instance;     // 静态单例变量

};

// 创建单例

Singleton Singleton::m_Instance;

 

// 单例操作实现

Singleton &Singleton::Instance()

{

    return m_Instance;

}

因为是类的静态变量,上面的代码在进程刚开始启动时就实例化了Singleton对象。

这有两个缺点:

n  初始化工作在程序一启动时就已经完成,如果静态函数到程序退出时都没有调用,我们的初始化工作就白做了;

n  初始化顺序无法确定,这可能会遇到麻烦,例如:

#include “Singleton.h”

Int global = Singleton::Instance()->DoSomething();

由于无法保证编译器先初始化m_Instance,再初始化global全局变量,所以有可能在程序启动时就发生异常。

    因此,虽然上述实现简单,但建议抵制住这种诱惑。

 

1.1.3  Meyers Singleton

对于上一节讨论的指针实现方法,我们有一个问题一直没有讨论,就是Singleton对象何时析构的问题:Singleton对象应该在何时摧毁自身实体?GoF著作中并没有讨论这个主题,事实上,这个问题确实很棘手。

其实,就算Singleton未被删除,也不会造成内存泄露。此外,当一个进程终止时,所有现代操作系统都能够将进程所用到的内存完全释放。

然而,泄露可能还是存在的,而且更隐蔽更有害,那就是资源泄漏。这是因为Singleton的构造函数可以索求广泛的资源:网络连接,OS的内核对象,IPC方法中的各种句柄,数据库连接等。

避免资源泄漏的唯一正确方法是在程序关闭时期删除Singleton对象。

对于静态变量的初始化方法,有一个改进的版本,通过利用了编译器的奇特技巧,可以摧毁Singleton对象。

Singleton &Singleton::Instance()

{

    static Singleton m_Instance;     // 静态单例变量

    return m_Instance;

}

这一手法是由Scott MeyersMeyers 1996a,条款26最先提出,所以被称为Meryars singleton函数内声明的静态变量函数内的static对象,在函数第一次被调用时初始化

编译器会产生一些代码,保证在进程退出前析构函数会被调用。实际上在编译器产生的代码中会调用atexit函数,把单实例的析构函数注册到atexit中,保证在进程退出前会调用这个析构函数。

Atexit函数内部维护了一个堆栈,先压入栈的函数指针会后执行。

 

我们自己也可以显示的在代码中调用atexit,以下是实现代码。

class Singleton

{

   

private:

    static void FreeInstance(void);  // 释放单例变量函数

};

// 单例操作实现

Singleton *Singleton::Instance()

{

    if (NULL == m_Instance)

    {

        m_Instance = new Singleton();

        // 用系统函数atexit告诉系统在程序退出的时候调用FreeInstance

        atexit(FreeInstance);

    }

    return m_Instance;

}

 

// 释放单例变量函数

void Singleton::FreeInstance(void)

{

    if (NULL != m_Instance)

    {

        delete m_Instance;

        m_Instance = NULL;

    }

}

1.1.4  Dead Reference问题

Meryers singleton是一种很好的Singleton的实现方式,大多数情况下,我们用这种方法实现Singleton就足够了,但有些情况下还是会有问题。

举个例子,假设有个程序使用了三个SingletonsKeyboard,DisplayLog。我们以Meryers singleton来实现这三个Singletons。假设KeyboardDisplay先于Log实例化,那么按照atexit后进先出的原则,在进程退出时Log应该先被析构,假设Log析构完成后,Keyboard析构函数调用时发生错误,这时需要调用Log来记日志,但Log已经被析构了,所以程序会崩溃,这就是Dead Reference问题。

首先我们看一个简单的解决方案:

// Singleton.h

class Singleton

{

public:

Singleton& Instance()

{

if (!pInstance_)

{

// Check for dead reference

if (destroyed_)

{

OnDeadReference();

}

else

{

// First call—initialize

Create();

}

}

return pInstance_;

}

private:

// Create a new Singleton and store a

// pointer to it in pInstance_

static void Create();

{

// Task: initialize pInstance_

static Singleton theInstance;

pInstance_ = &theInstance;

}

// Gets called if dead reference detected

static void OnDeadReference()

{

throw std::runtime_error("Dead Reference Detected");

}

virtual ~Singleton()

{

pInstance_ = 0;

destroyed_ = true;

}

// Data

Singleton pInstance_;

bool destroyed_;

... disabled 'tors/operator= ...

};

// Singleton.cpp

Singleton* Singleton::pInstance_ = 0;

bool Singleton::destroyed_ = false;

当出现Dead Reference问题时,程序会直接抛出C++异常,这个方案廉价,简单并且不损失效率。

1.1.5  Phoenix Singleton

上一节的方案虽然消除了不确定行为,但这个解决方案不能令人满意。

《现代C++设计》首先试图通过Phoenix Singleton来解决这个问题,所谓Phoenix,就是传说中可以不断从它自己的灰烬中重生的凤凰鸟。如果能够把Log设计成这样类型的Singleton,就可以解决问题。

class Singleton

{

... as before ...

void KillPhoenixSingleton(); // Added

};

void Singleton::OnDeadReference()

{

// Obtain the shell of the destroyed singleton

Create();

// Now pInstance_ points to the "ashes" of the singleton

// - the raw memory that the singleton was seated in.

// Create a new singleton at that address

new(pInstance_) Singleton;

// Queue this new object's destruction

atexit(KillPhoenixSingleton);

// Reset destroyed_ because we're back in business

destroyed_ = false;

}

void Singleton::KillPhoenixSingleton()

{

// Make all ashes again

// - call the destructor by hand.

// It will set pInstance_ to zero and destroyed_ to true

pInstance_->~Singleton();

}

看起来代码没有问题,但实际上并不成功,不成功的原因是C++标准的一个漏洞引起的。这个漏洞的意思是,当程序在atexit的栈中调用登记的函数时,这个函数本身不能调用atexit函数,否则会产生不可预料的后果(轻则资源泄漏,重则程序崩溃)。

1.1.6  实现“带寿命的Singletons

    对于Dead Reference问题,《现代C++设计》给出了第二种解决方案,使用带寿命的Singletons

如果可以在程序中设置Singleton的寿命,保证Log的寿命长于DisplayKeyboard的寿命就可以了。类似于下面的代码:

// This is a Singleton class

class SomeSingleton { ... };

// This is a regular class

class SomeClass { ... };

SomeClass* pGlobalObject(new SomeClass);

int main()

{

SetLongevity(&SomeSingleton().Instance(), 5);

// Ensure pGlobalObject will be deleted

// after SomeSingleton's instance

SetLongevity(pGlobalObject, 6);

...

}

带寿命的Singleton实现有些复杂,实现代码如下:

namespace Private

{

class LifetimeTracker

{

public:

LifetimeTracker(unsigned int x) : longevity_(x) {}

virtual ~LifetimeTracker() = 0;

friend inline bool Compare(

unsigned int longevity,

const LifetimeTracker* p)

{ return p->longevity_ > longevity; }

private:

unsigned int longevity_;

};

// Definition required

inline LifetimeTracker::~LifetimeTracker() {}

}

 

namespace Private

{

typedef LifetimeTracker** TrackerArray;

extern TrackerArray pTrackerArray;

extern unsigned int elements;

}

 

//Helper destroyer function

template <typename T>

struct Deleter

{

static void Delete(T* pObj)

{ delete pObj; }

};

// Concrete lifetime tracker for objects of type T

template <typename T, typename Destroyer>

class ConcreteLifetimeTracker : public LifetimeTracker

{

public:

ConcreteLifetimeTracker(T* p,unsigned int longevity,Destroyer d)

:LifetimeTracker(longevity),

,pTracked_(p)

,destroyer_(d)

{}

~ConcreteLifetimeTracker()

{

destroyer_(pTracked_);

}

private:

T* pTracked_;

Destroyer destroyer_;

};

void AtExitFn(); // Declaration needed below

}

template <typename T, typename Destroyer>

void SetLongevity(T* pDynObject, unsigned int longevity,

Destroyer d = Private::Deleter<T>::Delete)

{

TrackerArray pNewArray = static_cast<TrackerArray>(

std::realloc(pTrackerArray, sizeof(T) * (elements + 1)));

if (!pNewArray) throw std::bad_alloc();

pTrackerArray = pNewArray;

LifetimeTracker* p = new ConcreteLifetimeTracker<T, Destroyer>(

pDynObject, longevity, d);

TrackerArray pos = std::upper_bound(

pTrackerArray, pTrackerArray + elements, longevity, Compare);

std::copy_backward(pos, pTrackerArray + elements,

pTrackerArray + elements + 1);

*pos = p;

++elements;

std::atexit(AtExitFn);

}

 

static void AtExitFn()

{

assert(elements > 0 && pTrackerArray != 0);

// Pick the element at the top of the stack

LifetimeTracker* pTop = pTrackerArray[elements - 1];

// Remove that object off the stack

// Don't check errors-realloc with less memory

// can't fail

pTrackerArray = static_cast<TrackerArray>(std::realloc(

pTrackerArray, sizeof(T) * --elements));

// Destroy the element

delete pTop;

}

 

1.1.7  多线程问题

前面的实现代码还没有考虑多线程问题,实际绝大部分情况,程序都是运行在多线程环境下的。

Singleton加锁的代码可以像以下这样的简单方式:

Singleton& Singleton::Instance()

{

// mutex_ is a mutex object

// Lock manages the mutex

Lock guard(mutex_);

if (!pInstance_)

{

pInstance_ = new Singleton;

}

return *pInstance_;

}

这个好用,但效率不佳。

使用所谓的双检测锁定技术可以解决效率问题:

Singleton& Singleton::Instance()

{

if (!pInstance_) // 1

{

Guard myGuard(lock_);  // 2

if (!pInstance_) // 3

{

pInstance_ = new Singleton; // 4

}

}

return *pInstance_;

}

即使使用双检测锁定技术,在实践中代码也不总是正确。有些时候编译器会对上述代码进行优化,从而改变代码执行顺序,导致锁定失效。

为了避免编译器进行不必要的优化,需要在pInstance_声明前添加volatile修饰词。

 

1.1.8  在动态库中使用Singleton

在动态库中使用Singleton,与动态库中的静态变量的生命周期相关,经过实践,可以得出结论,我们在前面讨论的Singleton技术在动态库下运行是没有问题的。

在动态库被卸载的时候,atexit的栈中保存的指针会被调用,可以保证析构函数被执行。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值