设计模式——性能问题及其经典案例

本文深入探讨了C++中 Singleton 模式的实现,包括线程安全问题、双检查锁的缺陷以及如何通过内存屏障和原子操作避免这些问题。此外,还介绍了享元模式(Flyweight)的应用,用于优化大量细粒度对象的内存使用。内容涵盖了多线程编程中的内存管理、同步原语和设计模式的实战应用。
摘要由CSDN通过智能技术生成

性能问题

面对对象编程部很好解决了抽象接口的问题,但是不可避免要付出一定代价,也就是内存空间消耗,通常情况这种代价都可以忽略不计,但是在某些时候这种代价必须谨慎处理。
典型模式:
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查找一个文件中所有字符串的不同类型字体的个数;那么其实就是如果我们有现成的,就修改,没有,就加一个,其实思想是很简单的。

/* * 原始需求背景: * 网宿CDN要按月收取客户的服务费用,根据流量的大小、 * 服务的类型等,收取不同的费用,收费规则如下: * web应用:1000元/M * 流媒体应用:1000元/M*0.7 * 下载应用:1000元/M*0.5 * 月末打印报表时,要罗列每个用户每个频道的费用、客户总费用, * 还要打印该客户的重要性指数,重要性指数=网页流/100+下载流量/600; * * 需求变更场景: * 系统已经开发出来了,接下来,运维部门现在希望对系统做一点修改, * 首先,他们希望能够输出xml,这样可以被其它系统读取和处理,但是, * 这段代码根本不可能在输出xml的代码中复用report()的任何行为,唯一 * 可以做的就是重写一个xmlReport(),大量重复report()中的行为,当然, * 现在这个修改还不费劲,拷贝一份report()直接修改就是了。 * 不久,成本中心又要求修改计费规则,于是我们必须同时修改xmlReport() * 和report(),并确保其一致性,当后续还要修改的时候,复制-黏贴的问题就 * 浮现出来了,这造成了潜在的威胁。 * 再后来,客服部门希望修改服务类型和用户重要性指数的计算规则, * 但还没决定怎么改,他们设想了几种方案,这些方案会影响用户的计费规则, * 程序必须再次同时修改xmlReport()和report(),随着各种规则变得越来越复杂, * 适当的修改点越 来越难找,不犯错误的机会越来越少。 * 现在,我们运用所学的OO原则和方法开始进行改写吧。 */
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

无情の学习机器

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值