性能问题
面对对象编程部很好解决了抽象接口的问题,但是不可避免要付出一定代价,也就是内存空间消耗,通常情况这种代价都可以忽略不计,但是在某些时候这种代价必须谨慎处理。
典型模式:
Singleton
Flyweight
Singleton(单例模式)
动机:
在软件设计过程中,有一些特殊有的类,在软件运行过程中只允许存在一个实例,以保证逻辑正确和良好的效率。
作为设计者,那么如何保证使用者在使用过程中绕过常规的构造器实现一种机制来保证一个实例。
上代码:
class singleton
{
private:
singleton();
singleton(const singleton& others);
public:
static singleton* getsingleton();
static singleton* m_instance;
};
singleton* singleton::m_instance = nullptr;
//线程非安全版本
singleton* singleton::getsingleton()
{
return m_instance ? m_instance : new singleton();
}
通过这种方式,比如说我们想得到该对象本身,那么我们就无法调用构造函数来得到,只能用getsingleton函数,而这个函数一旦初始化之后,反复调用也不会得到其他的新的对象,这样就达到了目的;
但是如标注所说,这个版本并没有实现线程安全,比如说
对于线程一,第一次调用getsingleton的时候表示需要创建新的对象;
对于线程二,在线程一还没有创建但是已经进入判断语句的时候,假如也进入判断语句,这样就会产生很多个对象,多线程并不安全。
当然了,我们这里,可以用使用比如说自旋锁,互斥锁等方式保证同一时间只有一个线程能够访问资源,比如这样
//线程安全版本
#include <mutex>
std::mutex mtx;
singleton* singleton::getsingleton()
{
std::lock_guard<std::mutex> lck(mtx);
return m_instance ? m_instance : new singleton();
}
这样一来确实实现了线程安全版本,但是不可避免,锁需要代价,即共享内存的上下文切换,不管是自旋还是互斥都会由相应的代价。
但是我们仔细观擦发现,多个线程同时进入这个文件,可能只有一个文件在写,很多个文件在读,那么其实读是不需要加锁的,因此加锁就造成了浪费。
当然了,我们也能用用读写锁来进一步优化,比如公平读写锁,读优先或者写优先等等,但是有没有什么其他办法避免这个代价呢?
有一种概念叫双检查锁,可以避免上述开销,具体代码如下:
singleton* singleton::getsingleton()
{
if (!m_instance)
{
std::lock_guard<std::mutex> lck(mtx);
if (!m_instance)
{
m_instance = new singleton();
}
}
return m_instance;
}
这里要说明一下为什么要两个判断,因为如果把第二个去掉,那么其实根本锁不住,具体原因各位可以自己想想;
这种方式貌似已经很完美了,但是,其实由于内存的读写问题,会出现reorder现象;
reorder其实就是说白了,汇编语言的有序性跟实际我们写代码是由一定差异的,计算机内部会对汇编的执行语句进行一个重排,这就是reorder,双锁设计并不能保证多线程的设计原则之一,有序性。
具体来说,在程序进行到这一行的时候,这一行代码并不具有原子性。
具体多线程编码原则不在设计模式讨论范围内。
m_instance = new singleton();
我们大概要执行三步:
1 先去内存池中索取内存;
2 .调用构造器,给内存块中内容赋值
3.返回内存块中头指针复制给m_instance
具体分配流程也不在本文讨论范围内。
这三步在我们认为看来,是有执行顺序的,但是在汇编中,顺序可能不是这样,变成可能执行132;这样一来,我们其实已经已经分配了内存,但是得到的指针却是空指针。进而,我们仍然可能生成了两个对象。
放个示意图
熟悉java或者c#的朋友其实很容易就能想到解决办法,加volatile关键字就能保证有序性;
在c++11之后引入了一种跨平台的volatile类似的实现机制:
#include <mutex>
#include <atomic>
std::atomic<singleton*> m_instance;//这一步其实就相当于volatile
std::mutex mtx;
singleton* singleton::getsingleton()
{
singleton* tmp = m_instance.load(std::memory_order_relaxed);
//加载原子对象中存入的值,等价与直接使用原子变量。
std::_Atomic_thread_fence(std::memory_order_acquire);
//获取每一步边界,防止reorder
if (!tmp)
{
std::lock_guard<std::mutex> lck(mtx);
tmp = m_instance.load(std::memory_order_relaxed);
if (!tmp)
{
m_instance = new singleton();
std::_Atomic_thread_fence(std::memory_order_release);
//释放内存的边界
m_instance.store(tmp, std::memory_order_relaxed);
//存储一个值到原子对象,等价于使用等号。
}
}
return tmp;
}
具体的c艹11多线程编程实现不在本文讨论范围内
总结:
其实singleton是最简单的一种模式,但是因为双检查锁的一些问题,因此需要通过特殊的语法实现,因此这点需要特别注意。
flyweight(享元模式)
动机:
软件系统中,纯粹对象方案问题在于大量的细粒度对象会充斥在系统中,从而对内存要求很高,
那么作为开发人员,如何避免大量细粒度问题的同时,能够透明地让客户采用面对对象进行操作呢?
解决方案:利用共享的方式可以有效支持大量细粒度的对象,这种思路在其他地方也有广泛应用比如线程池,IO复用高并发网络模型,常量池等等;
上代码:
#include<string>
#include<map>
using std::map;
using std::string;
class font
{
private:
//unicque object key
string key;
//object state
//...
public:
font(const string& key)
{
//...
}
};
class fontfactory
{
private:
map<string, int> fontpool;
public:
void checkfile(const string path)
{
for each (string tmp in openfile(path))
{
countfont(tmp);
}
}
font* countfont(const string& key)
{
auto it = fontpool.find(key);
if (it != fontpool.end())
{
fontpool[key]++;
return NULL;
}
else
{
font* newfont = new font(key);
fontpool[key] = 0;
return newfont;
}
};
};
上述代码,意思是比如说我们想通过checkfile查找一个文件中所有字符串的不同类型字体的个数;那么其实就是如果我们有现成的,就修改,没有,就加一个,其实思想是很简单的。