通常情况下,应减小同步的范围,使系统获得更好的性能。简单将整个方法标记为同步不是一个好主意,除非能确定方法中的每个代码都需要受同步保护。 1.3.1.2 同步策略 使用 lock 进行同步,同步对象可以选择 Type、this 或为同步目的专门构造的成员变量。 避免锁定Type★ 锁定Type对象会影响同一进程中所有AppDomain该类型的所有实例,这不仅可能导致严重的性能问题,还可能导致一些无法预期的行为。这是一个很不好的习惯。即便对于一个只包含static方法的类型,也应额外构造一个static的成员变量,让此成员变量作为锁定对象。 避免锁定 this 锁定 this 会影响该实例的所有方法。假设对象 obj 有 A 和 B 两个方法,其中 A 方法使用 lock(this) 对方法中的某段代码设置同步保护。现在,因为某种原因,B 方法也开始使用 lock(this) 来设置同步保护了,并且可能为了完全不同的目的。这样,A 方法就被干扰了,其行为可能无法预知。所以,作为一种良好的习惯,建议避免使用 lock(this) 这种方式。 使用为同步目的专门构造的成员变量 这是推荐的做法。方式就是 new 一个 object 对象, 该对象仅仅用于同步目的。 如果有多个方法都需要同步,并且有不同的目的,那么就可以为些分别建立几个同步成员变量。 1.3.1.4 集合同步 C#为各种集合类型提供了两种方便的同步机制:Synchronized 包装器和 SyncRoot 属性。
1.3.3 多线程编程技巧 1.3.3.1 使用 Double Check 技术创建对象
1.4.2.3 避免装箱和拆箱 C#可以在值类型和引用类型之间自动转换,方法是装箱和拆箱。装箱需要从堆上分配对象并拷贝值,有一定性能消耗。如果这一过程发生在循环中或是作为底层方法被频繁调用,则应该警惕累计的效应。 一种经常的情形出现在使用集合类型时。例如:
推荐下面的写法:
其实这种写法非常自然,而且效率很高,完全不需要用个Remove方法绕来绕去。 1.7.3 避免两次检索集合元素 获取集合元素时,有时需要检查元素是否存在。通常的做法是先调用ContainsKey(或Contains)方法,然后再获取集合元素。这种写法非常符合逻辑。 但如果考虑效率,可以先直接获取对象,然后判断对象是否为null来确定元素是否存在。对于Hashtable,这可以节省一次GetHashCode调用和n次Equals比较。
如下面的示例:
效率更高的做法如下:
1.8 Hashtable Hashtable是一种使用非常频繁的基础集合类型。需要理解影响Hashtable的效率有两个因素:一是散列码(GetHashCode方法),二是等值比较(Equals方法)。Hashtable首先使用键的散列码将对象分布到不同的存储桶中,随后在该特定的存储桶中使用键的Equals方法进行查找。良好的散列码是第一位的因素,最理想的情况是每个不同的键都有不同的散列码。Equals方法也很重要,因为散列只需要做一次,而存储桶中查找键可能需要做多次。从实际经验看,使用Hashtable时,Equals方法的消耗一般会占到一半以上。
这里强调一下在方法之间传递连接的危害:曾经在压力测试中遇到过一个测试案例,当增大用户数的时候,这个案例要比别的案例早很久就用掉连接池中的所有连接。经分析,就是因为A方法把一个打开的连接传递到了B方法,而B方法又调用了一个自行打开和关闭连接的C方法。在A方法的整个运行期间,它至少需要占用两条连接才能够成功工作,并且其中的一条连接占用时间还特别长,所以造成连接池资源紧张,影响了整个系统的可伸缩性! 2.2.2 显式关闭连接 Connection对象本身在垃圾回收时可以被关闭,而依赖垃圾回收是很不好的策略。推荐使用using语句显式关闭连接,如下例:
2.4.3 使用类型化方法访问属性 从Row中访问某列属性,用GetString、GetInt32这种显式指明类型的方法,其效率较通用的GetValue方法有细微提高,因为不需要做类型转换。 2.4.4 使用多数据集 部分场景可以考虑一次返回多数据集来降低网络交互次数,提升效率。示例如下:
在C++中封装的概念是把一个对象的外观接口同实际工作方式(实现)分离开来,但是C++的封装是不完全的,编译器必须知道一个对象的所有部分的声明,以便创建和管理它。我们可以想象一种只需声明一个对象的公共接口部分的编程语言,而将私有的实现部分隐藏起来。C + +在编译期间要尽可能多地做静态类型检查。这意味着尽早捕获错误,也意味着程序具有更高的效率。然而这对私有的实现部分来说带来两个影响:一是即使程序员不能轻易地访问实现部分,但他可以看到它;二是造成一些不必要的重复编译。
然而C++并没有将这个原则应用到二进制层次上,这是因为C++的类既是描述了一个接口同时也描述了实现的过程,示例如下:
CMyStirng对外过多的暴露了内存布局实现的细节,这些信息过度的依赖于这些成员变量的大小和顺序,从而导致了客户过度依赖于可执行代码之间的二进制耦合关系,这样的接口不利于跨语言跨平台的软件开发和移植。
1.1.1 Handle-Body模式
解决这个问题的技术有一种叫句柄类( handle classes)。有关实现的任何东西都消失了,只剩一个单一的指针“m_pThis”。该指针指向一个结构,该结构的定义与其所有的成员函数的定义都出现在实现文件中。这样,只要接口部分不改变,头文件就不需变动。而实现部分可以按需要任意更动,完成后只要对实现文件进行重新编译,然后再连接到项目中。
下面是这项技术的简单例子。头文件中只包含公共的接口和一个简单的没有完全指定的类指针。
这是所有客户程序员都能看到的。 class CMyString;
是一个没有完全指定的类型说明或类声明(一个类的定义包含类的主体)。它告诉编译器,CMyString是一个结构的名字,但没有提供有关该结构的任何东西。这对产生一个指向结构的指针来说已经足够了。但我们在提供一个结构的主体部分之前不能创建一个对象。在这种技术里,包含具体实现的结构主体被隐藏在实现文件中。
在设计模式中,这就叫做Handle-Body 模式,Handle-Body只含有一个实体指针,服务的数据成员永远被封闭在服务系统中。
Handle-Body的布局结构永远不会随着实现类数据成员的加入或者删除或者修改而导致Handle-Body的修改,即Handle-Body协议不依赖于C++实现类的任何细节。这就有效的对用户的编译器隐藏了这些细节,用户在使用对这项技术时候,Handle-Body 接口成了它唯一的入口。
然而Handle-Body模式也有自己的弱点:
1、接口类必须把每一个方法调用显示的传递给实现类,这在一个只有一个构造和一个析构的类来说显然不构成负担,但是如果一个庞大的类库,它有上百上千个方法时候,光是编写这些方法传递就有可能非常冗长,这也增加了出错的可能性。
3、由于句柄的存在,依然存在编译连接器兼容性问题。
1.1.2 抽象接口
使用了“接口与实现的分离”技术的 Handle-Body 解决了编译器/链接器的大部分问题,而C++面向对象编程中的抽象接口同样是运用了“接口与实现分离”的思想,而采用抽象接口对于解决这类问题是一个极其完美的解决方案。
1、抽象接口的语言描述:
class IMyString
virtual int Length() const = 0; //这表示是一个纯虚函数,具有纯虚函数的接口
virtual int Index(const char *psz) const = 0;
};
2、抽象接口的内存结构:
抽象接口采用虚函数表来调用成员方法。
3、 抽象接口的实现代码:
接口:
class IMyString
实现:
class CMyString:public IMyString
private:
const int m_cch;
char *m_psz;
public:
CMyString(const char *psz);
virtual ~CMyString();
int Length() const;
int Index(const char *psz) const;
从上面采用抽象接口的实例来看,抽象接口解决了Handle-Body所遗留下来的全部缺陷。
抽象接口的一个典型应用:
抽象工厂(AbstractFactroy)
1.2 多继承与菱形缺陷、this跳转等
多重继承是C++语言独有的继承方式,其它几乎所有语言都秉承了单一继承的思想。这是因为多重继承致命的缺陷导致的:
1.2.1 菱形缺陷
当继承基类时,在派生类中就获得了基类所有的数据成员副本。假如类B 从A1和A2两个类多重继承而来,这样B类就包含A1、A2类的数据成员副本。
考虑如果A1、A2都从某基类派生,该基类称为Base,现在继承关系将出现菱形继承关系。
class Base{… … };
class A1 :public Base {… … };
class A2 :public Base {… … };
class B :public A1,public A2 {… … };
那么A1、A2都具有Base的副本。这样B就包含了Base的两个副本,副本发生了重叠,不但增加了存储空间,同时也引入了二义性。这就是菱形缺陷,菱形缺陷的两个缺陷:
1、子对象重叠
2、向上映射的二义性。
菱形缺陷的其中一种解决办法是使用虚拟继承。
在C++世界里最广泛的使用虚拟继承解决菱形缺陷的应用便是标准C++的输入/输出iostream;
1.2.2 多重接口与方法名冲突问题(Siamese twins)
对继承而来的虚函数改写很容易,但是如果是在改写一个“在两个基类都有相同原型”的虚函数情况就不那么容易了。
提出问题:
假设汽车最大速度的接口为ICar,潜艇最大速度的接口为 IBoat,有一个两栖类的交通工具它可以奔跑在马路上,也可以航行在大海中,那么它就同时拥有ICar、IBoat两种交通工具的最大速度特性,我们定义它的接口为ICarBoat;
class ICar
virtual int GetMaxSpeed()= 0;
};
class IBoat
virtual int GetMaxSpeed()= 0;
};
我们先对ICarBoat的接口做一个尝试:
class CCarBoat
virtual int GetMaxSpeed();//既完成ICar的GetMaxSpeed()接口方法又 //完成IBoat的接口方法?显然不能够
};
解决问题:
显然上面这个尝试根本就无法成功,只用一个实现方法,怎么能够求出这个ICarBoat交通工具奔跑在马路上的最高时速,同时也能够求出航行在大海上的最大航行速度呢。
上面这一问题矛盾就在一一个方法,却需要两个答案。看来ICarBoat要返回两个答案就必须有两个方法了,我们假设一个方法是求在陆地上奔跑的速度,名称为GetCarMaxSpeed();另一个方法是求在大海上航行的最大速度,名称为GetBoatMaxSpeed();那这两个方法又怎么和GetMaxSpeed()接口方法联系起来呢;
幸运的是,我们找到了解决办法,而且解决办法有很多种,下面介绍一下继承法。
class IXCar :public ICar
virtual int GetMaxSpeed()
GetCarMaxSpeed();
virtual int GetCarMaxSpeed() = 0;
};
class IXBoat:public IBoat
virtual int GetMaxSpeed()
GetBoatMaxSpeed();
virtual int GetBoatMaxSpeed() = 0;
};
classCCarBoat: public IXCar , public IXBoat
virtual int GetCarMaxSpeed()
… …
virtual int GetBoatMaxSpeed()
… …
};
1.2.3 this跳转
this跳转是指的“对象同一性”问题。
在单一继承的世界内,无论继承关系怎么复杂,针对于同一对象,无论它的子类或者父类的this指针永远相等。即如果 B从A继承,那么 对于一个已经实例化B类的对象 bObject,永远有(B*)&bObject ==(A*)&bObject成立。
但是在多继承的世界内,上面的等式就不能恒成立,对象的同一性受到了挑战。
特别的是,在多继承世界内如果菱形关系存在情况下,如果对于已经实例化B类的对象bObject; (Base*)(A1*)&bObject != (Base*)(A2*)&bObject 成立,当这种事情发生的时候我们就只能特殊处理了。这种情况在COM应用中处处都会发生。
1.3 C++多态的两种多态形式和区别
C++有两种多态多态形式:
1、编译时刻多态,编译时刻多态依靠函数重载或者模板实现
2、运行时刻多态。运行时刻多态依靠需函数虚接口实现
virtual int Length() const = 0; //这表示是一个纯虚函数,具有纯虚函数的接口
virtual int Index(const char *psz) const = 0;