C#学习资料
第一章 为什么要进行知识总结
大家都做过很多的项目。但是每个项目做完后,你问我我会了什么我真的不能回答你们。最近公司很闲,我就在思考,对于c#这个语言来说我缺什么。然后我自己给自己的答案是我缺少对知识的总结。我就从基础的知识到一些高深的点的知识进行部分的总结。慢慢的完善,参考了很多大牛的博客。
如果你能够把总结的知识都很熟练的运用的你的开中的话,我想信你无论是在哪个公司你都是公司中的大牛。里面包括了很多的知识,当然总结的只是部分的知识。一门语言有很多你要掌握的东西。当然你看的时候要有一定的专业知识,要用联想的方式去看才能收到效果。总结的很杂。
好了,废话少说,我们开干。
第二章:c#基础知识
1:结构
结构作为参数传递时,是值传递
结构的构造函数必须有参数
结构实例化可以不用NEW
结构不能继承,但是可以实现接口
结构不能初始化实例字段
2:委托
委托是一个类,它定义了方法的类型,可以讲方法当做另一个方法的参数。
3:常用的修饰符
private 私有的,当前类访问,子类获取其他类均不可调用,子类也不能继承private的属性或方法
public 完全公开的,只要在一个项目中都能访问。如果其他程序集要访问,必须添加该程序集
protected 和private是一样的,区别在于,子类可以继承protected的属性或方法
internal 公开的,统一程序集中都可以访问
4)、this关键字:
this顾名思义,就是指本类中的意思,引用当前类的成员。当然如果程序在运行中,则可以精确地说,
this指当前类的对象的成员,作用就是用来区分对象的。
因为一个类可以有N个对象。不过在static类中不能使用this关键字,
究其原因,无非是static不可能实例化多个对象,它只有一个,自然没必要去用this来区分对象了。
5 base关键字:
一般用于,子类访问父类。
一种是,重写父类方法时,
6:反射(Reflection)是.NET中的重要机制,通过反射,可以在运行时获得.NET中每一个类型(包括类、结构、委托、接口和枚举等)的成员,包括方法、属性、事件,以及构造函数等
7: 密封类是指用sealed关键字修饰的一种类,它的目的是防止派生,也就是这种类不能被继承。
特点:
不能用作基类,不能抽象,密封类的调用比较快。
8:抽象类和接口的区别:
a、类是对对象的抽象,可以把抽象类理解为把类当作对象,抽象成的类叫做抽象类。而接口只是一个行为的规范或规定,微软的自定义接口总是后带able字段,证明其是表述一类类“我能做...”.抽象类更多的是定义在一系列紧密相关的类间,而接口大多数是关系疏松但都实现某一功能的类中;
b、接口基本上不具备继承的任何具体特点,它仅仅承诺了能够调用的方法;
c、一个类一次可以实现若干个接口,但是只能扩展一个父类;
d、接口可以用于支持回调,而继承并不具备这个特点;
e、抽象类不能被密封;
f、抽象类实现的具体方法默认为虚的,但实现接口的类中的接口方法却默认为非虚的,当然也可以声明为虚的;
g、(接口)与非抽象类类似,抽象类也必须为在该类的基类列表中列出的接口的所有成员提供它自己的实现。但是,允许抽象类将接口方法映射到抽象方法上;
h、抽象类实现了oop中的一个原则,把可变的与不可变的分离。抽象类和接口就是定义为不可变的,而把可变的交给子类去实现;
i、好的接口定义应该是具有专一功能性的,而不是多功能的,否则造成接口污染。如果一个类只是实现了这个接口的中一个功能,而不得不去实现接口中的其他方法,就叫接口污染;
j、如果抽象类实现接口,则可以把接口中方法映射到抽象类中作为抽象方法而不必实现,而在抽象类的子类中实现接口中方法。
9:泛型
所谓泛型,是将类型参数的概念引入到.NET,通过参数化类型来实现在同一份代码上操作多种数据类型。是引用类型,是堆对象
功能特性如下:
a、避免装箱拆箱,提高了性能;
b、提高了代码的重用性;
c、类型安全的,因为在编译的时候会检测;
d、可以创建自己的泛型接口、泛型类、泛型方法、泛型事件和泛型委托。
10:类
类,是面向对象语言的基础。类的三大特性:封装、继承、多态。最基本的特性就是封装性。
程序员用程序描述世界,将世界的所有事物都看成对象,怎么描述这个对象?那就是类了。也就是用类来封装对象。用书上的话说,类是具有相同属性和行为的对象的抽象。
宝马汽车、别克汽车、五菱之光汽车... 基本具有相同的属性和行为,所以可以抽象一个汽车类,当然也可以把路人甲的宝马汽车、路人乙的别克汽车... 抽象一个汽车类。
类抽象完成后,可以实例化,实例化后的称之为一个对象,然后可以对属性赋值或运行类的方法。属性和方法同每个对象关联,不同的对象有相同的属性,但属性值可能不同;
也具有相同的方法,但方法运行的结果可能不同。
类的属性和方法是被类封装的。
11:继承
继承实际是一个类对另一个类的扩展,后者称之为基类,前者称之为子类。继承就是子类拥有基类的一切属性和方法,子类还可以增加属性和方法。但是子类不能去掉父类的属性和方法。
当然这里还要提到修饰符的问题,子类拥有基类的所有属性和方法,不意味着子类可以任意访问继承的这些属性和方法。子类只能访问到public和protected修饰的属性和方法,
其余无法直接访问。还有一个就是static的属性和方法是不能被继承下来的,因为静态类型之和类有关系与对象无关。
继承需要注意一点,就是子类的构造器问题。程序运行过程是子类首先调用父类的构造器。如果子类没有写构造器,那么就会调用父类的默认构造器。
如果父类没有默认的构造器,即父类写了带参数的构造器,那么子类也就调用带参数的那个构造器,不过需要指明是调用的哪个带参数的构造器(见代码中base)。
12:数组
ICollection:
该接口为其实现类定义了两个主要规范:
(1)集合元素个数,即Count属性;
(2)迭代(GetEnumerator方法)。
GetEnumertor方法时由ICollection的父接口IEumerable继承而来的。ICollection接口定义了一个存储和获取object类型对象引用的集合,这样可以存储和获取各种引用类型对象的引用或值类型对象。
IEnumerable接口:
该接口是ICollection的父接口,这个接口为其实现的类提供了可迭代的能力。IEumerable接口只有一个GetEnumerator方法,返回一个循环访问集合的枚举数
13:迭代
迭代,也叫迭代器,也就是设计模式中的迭代模式,意义是:提供一种方法顺序访问一个聚合对象中各个元素,而又不暴露该对象的内部表示。因为集合的存储方式各不相同,List是连续存储,链表使用对象间的引用存储... ...那么当需要遍历集合时就不方便,就需要一种能遍历所有不同的集合的方法,这就有了迭代器。
14 :IList接口概述
ILis接口从ICollection接口继承,具备以下特性,
Count属性——获取集合元素个数;
GetEnumerator方法——可以迭代;
CopyTo方法——将指定元素复制到另一个数组中;
Clear方法——清空整个集合。
IList新增特性,
索引器属性——根据索引访问集合中任意元素;
Add方法——向集合末尾添加元素;
Insert方法——向集合指定位置插入元素;
Remove方法——移除指定元素;(包括RemoveAt)
Contains方法——判断对象是否在集合中;
IndexOf方法——查找指定对象在集合中的索引位置。
另外,IList接口集合按照顺序存放元素,不改变元素存放顺序。
ArrayList是继承IList的,List继承IList<T>:
ArrayList类处于System.Collection命名空间下;
List<T>类处于System.Collection.Specialized命名空间下。
15:继承与隐藏方法
public abstract class abstractlist
{
public abstractlist()
{
Console.WriteLine("A");
}
public void ceshi()
{
Console.WriteLine("A.FUN()");
}
}
public class Class2 : abstractlist
{
public Class2()
{
Console.WriteLine("B");
}
public new void ceshi()
{
Console.WriteLine("B.FUN()");
}
}
调用
Class2 b = new Class2();
b.ceshi();
abstractlist a = new Class2();
a.ceshi();
结果
A
B
B.FUN()
A
B
A.FUN()
解读:Class2继承abstractlist父类,如果父类有结构函数,就先执行父类的结构函数,然后执行子类的结构函数。
在子类中定义了一个与父类一样的方法必须用NEW修饰,如果不用NEW修饰出现异常。目的是隐藏子类的这个方法,除非你初始化NEW子类,然后调用子类的方法。
16:装箱与拆箱
int i = 2000;
object o = (object)i;
i= 2001;
int j = (int)o;
Console.WriteLine("{0},{1},{2}", i, o, j);
结果:2001,2000,2000 解读:object o 为装箱的操作,int j为拆箱操作。
将值类型转换为引用类型,需要进行装箱操作(boxing):
1、首先从托管堆中为新生成的引用对象分配内存。
2、然后将值类型的数据拷贝到刚刚分配的内存中。
3、返回托管堆中新分配对象的地址。
可以看出,进行一次装箱要进行分配内存和拷贝数据这两项比较影响性能的操作。
将引用内型转换为值内型,需要进行拆箱操作(unboxing):
1、首先获取托管堆中属于值类型那部分字段的地址,这一步是严格意义上的拆箱。
2、将引用对象中的值拷贝到位于线程堆栈上的值类型实例中。
经过这2步,可以认为是同boxing是互反操作。严格意义上的拆箱,并不影响性能,但伴随这之后的拷贝数据的操作就会同boxing操作中一样影响性能。
17: 一个整形数组,找到第二大的值
privatestatic int get2rdMax(int[] ar)
{
int s_max = 0;
for (int i = 0; i < ar.Length; i++)
{
for (int j = 0; j <ar.Length; j++)
{
if (ar[j] > ar[i])
{
int temp = ar[i];
ar[i] = ar[j];
ar[j] = temp;
}
}
}
if (ar.Length >= 2)
{
s_max= ar[ar.Length - 2];
}
return s_max;
}
18: 一个字符串,反过来显示
private static string FanSzhu(string str)
{
string list = "";
for (int i = str.Length - 1; i >= 0; i--)
{
list += str[i].ToString();
}
return list;
}
19:mysql中查找30-40记录,30与40的ID不连续
SELECT * FROM (SELECT * FROMclientrelation ORDER BY id LIMIT 40 ) t WHERE t.id>=30 ANDt.id<=40
20: ref 与out区别
ref和out都是C#中的关键字,所实现的功能也差不多,都是指定一个参数按照引用传递。
对于编译后的程序而言,它们之间没有任何区别,也就是说它们只有语法区别。
总结起来,他们有如下语法区别:
1、ref传进去的参数必须在调用前初始化,out不必,即:
int i;
SomeMethod( ref i );//语法错误
SomeMethod( out i );//通过
2、ref传进去的参数在函数内部可以直接使用,而out不可:
public void SomeMethod(ref int i)
{
int j=i;//通过
//...
}
public void SomeMethod(out int i)
{
int j=i;//语法错误
}
3、ref传进去的参数在函数内部可以不被修改,但out必须在离开函数体前进行赋值。
ref在参数传递之前必须初始化;而out则在传递前不必初始化,且在... 值类型与引用类型之间的转换过程称为装箱与拆箱。
总结:
应该说,系统对ref的限制是更少一些的。out虽然不要求在调用前一定要初始化,但是其值在函数内部是不可见的,也就是不能使用通过out传进来的值,并且一定要在函数内赋一个值。或者说函数承担初始化这个变量的责任。
下面谈谈ref和out到底有什么区别:
1 关于重载
原则:有out|ref关键字的方法可以与无out和ref关键字的方法构成重载;但如想在out和ref间重载,编译器将提示:不能定义仅在ref和out的上的方法重载
2 关于调用前初始值
原则:ref作为参数的函数在调用前,实参必须赋初始值。否则编译器将提示:使用了未赋值的局部变量;
out作为参数的函数在调用前,实参可以不赋初始值。
3 关于在函数内,引入的参数初始值问题
原则:在被调用函数内,out引入的参数在返回前至少赋值一次,否则编译器将提示:使用了未赋值的out参数;
在被调用函数内,ref引入的参数在返回前不必为其赋初值。
总结:C#中的ref和out提供了值类型按引用进行传递的解决方案,当然引用类型也可以用ref和out修饰,但这样已经失去了意义。因为引用数据类型本来就是传递的引用本身而非值的拷贝。ref和out关键字将告诉编译器,现在传递的是参数的地址而不是参数本身,这和引用类型默认的传递方式是一样的。同时,编译器不允许out和ref之间构成重载,又充分说明out和ref的区别仅是编译器角度的,他们生成的IL代码是一样的。有人或许疑问,和我刚开始学习的时候一样的疑惑:值类型在托管堆中不会分配内存,为什么可以按地址进行传递呢?值类型虽然活在线程的堆栈中,它本身代表的就是数据本身(而区别于引用数据类型本身不代表数据而是指向一个内存引用),但是值类型也有它自己的地址,即指针,现在用ref和out修饰后,传递的就是这个指针,所以可以实现修改后a,b的值真正的交换。这就是ref和out给我们带来的好处。
首先:两者都是按地址传递的,使用后都将改变原来参数的数值。
其次:rel可以把参数的数值传递进函数,但是out是要把参数清空,就是说你无法把一个数值从out传递进去的,out进去后,参数的数值为空,所以你必须初始化一次。这个就是两个的区别,或者说就像有的网友说的,rel是有进有出,out是只出不进。
正在完善中。。。。。。。
第三章 设计模式
1:简单工厂模式
返回具有同样方法的类的实例,它们可以是不同的派生子类的实例,也可以是实际上毫无关系仅仅是共享相同的接口的类。
不管是那一种形式,这些类实例中的方法必须是相同的,并且能够被交替使用。
参与者:
· 工厂角色(Creator)
是简单工厂模式的核心,它负责实现创建所有具体产品类的实例。工厂类可以被外界直接调用,创建所需的产品对象。
· 抽象产品角色(Product)
是所有具体产品角色的父类,它负责描述所有实例所共有的公共接口。
· 具体产品角色(ConcreteProduct)
继承自抽象产品角色,一般为多个,是简单工厂模式的创建目标。工厂类返回的都是该角色的某一具体产品。
1: 抽象产品角色
//·抽象产品角色(Product)
publicabstract class CreateFactroy
{
public CreateFactroy()
{ }
public abstract void Excute(string str);
}
2: 具体产品角色
// 具体产品角色(ConcreteProduct)
public class CreateClothes : CreateFactroy
{
public CreateClothes()
{ }
public override void Excute(string str)
{
Console.WriteLine(str+"大衣");
}
}
3:具体产品角色
// 具体产品角色(ConcreteProduct)
public class CreateHeads : CreateFactroy
{
public CreateHeads()
{ }
public override void Excute(string str)
{
Console.WriteLine(str + "头饰");
}
}
4:工厂角色
//· 工厂角色(Creator)
public static CreateFactroyGetCreateFactroy(int key)
{
if (key == 1)
{
return new CreateClothes();
}
else
{
return new CreateHeads();
}
}
5:调用
private void button1_Click(object sender,EventArgs e)
{
CreateFactroy factroy=SelctFactroy.GetCreateFactroy(1);
factroy.Excute("时尚");
CreateFactroy factroy1 =SelctFactroy.GetCreateFactroy(2);
factroy1.Excute("普通");
}
总结:返回同样方法的类。
专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。它又称为静态工厂方法模式,属于类的创建型模式。简单工厂模式的UML类图(见右图) 简单工厂模式的实质是由一个工厂类根据传入的参数,
动态决定应该创建哪一个产品类(这些产品类继承自一个父类或接口)的实例。 该模式中包含的角色及其职责 工厂(Creator)角色 简单工厂模式的核心,它负责实现创建所有实例的内部逻辑。工厂类可以被外界直接调用,
创建所需的产品对象。 抽象(Product)角色 简单工厂模式所创建的所有对象的父类,它负责描述所有实例所共有的公共接口。 具体产品(ConcreteProduct)角色简单工厂模式的特点: 简单工厂模式的创建目标,
所有创建的对象都是充当这个角色的某个具体类的实例。 不难发现,简单工厂模式的缺点也正体现在其工厂类上,由于工厂类集中了所有实例的创建逻辑,所以“高内聚”方面做的并不好。另外,当系统中的具体产品类不断增多时,
可能会出现要求工厂类也要做相应的修改,扩展性并不很好。
2:工厂模式
参与者
· 抽象产品角色(Product)
定义产品的接口
· 具体产品角色(ConcreteProduct)
实现接口Product的具体产品类
· 抽象工厂角色(Creator)
声明工厂方法(FactoryMethod),返回一个产品
· 真实的工厂(ConcreteCreator)
实现FactoryMethod工厂方法,由客户调用,返回一个产品的实例
1:抽象产品角色(Product)
public abstract class CreateClothes
{
public abstract stringCreateJacket(string str);
public abstract stringCreatePants(string str);
}
2:具体产品角色(ConcreteProduct)
publicclass CreateCommandJackets : CreateClothes
{
public override stringCreateJacket(string str)
{
string result = "";
if (!string.IsNullOrWhiteSpace(str))
{
result = str + "普通上衣";
}
return result;
}
public override stringCreatePants(string str)
{
string result = "";
if(!string.IsNullOrWhiteSpace(str))
{
result = str + "普通裤子";
}
return result;
}
}
public class CreateJackets :CreateClothes
{
public override stringCreateJacket(string str)
{
string result = "";
if(!string.IsNullOrWhiteSpace(str))
{
result = str + "时尚上衣";
}
return result;
}
public override stringCreatePants(string str)
{
string result = "";
if(!string.IsNullOrWhiteSpace(str))
{
result = str + "时尚裤子";
}
return result;
}
}
3:抽象工厂角色(Creator)
public abstract class CreateFactory
{
public abstract CreateClothesCreatesCoat(int key);
}
4:真实的工厂(ConcreteCreator)
publicclass SelectFactory:CreateFactory
{
public override CreateClothesCreatesCoat(int key)
{
if (key == 1)
{
return new CreateJackets();
}
else
{
return newCreateCommandJackets();
}
}
}
5:简单调用
SelectFactory factory = new SelectFactory();
string str = "海澜之家";
Console.WriteLine(factory.CreatesCoat(1).CreateJacket(str));
Console.WriteLine(factory.CreatesCoat(2).CreateJacket(str));
Console.WriteLine(factory.CreatesCoat(2).CreatePants(str));
Console.WriteLine(factory.CreatesCoat(1).CreatePants(str));
优点:
· 基于工厂角色和产品角色的多态性设计是工厂方法模式的关键。它能够使工厂可以自主确定创建何种产品对象。而且如何创建一个具体产品的细节完全封装在具体工厂内部,符合高内聚,低耦合。
· 在系统中加入新产品时,无需修改抽象工厂和抽象产品提供的接口,无需修改客户端,也无需修改其他的具体工厂和具体产品,很好的利用了封装和委托。
缺点:
· 在添加新产品时,需要编写新的具体产品类(其实这不算一个缺点,因为这是不可避免的),要增加与之对应的具体工厂类。
3: 抽象工厂模式
1:抽象产品类
publicabstract class AbstractProductA {
//每个产品共有的方法
publicvoid shareMethod(){
}
//每个产品相同方法,不同实现
publicabstract void doSomething();
}
产品A1的实现类
publicclass ProductA1 extends AbstractProductA {
publicvoid doSomething() {
System.out.println("产品A1的实现方法");
}
}
产品A2的实现类
publicclass ProductA2 extends AbstractProductA {
publicvoid doSomething() {
System.out.println("产品A2的实现方法");
}
}
2:抽象工厂类
publicabstract class AbstractCreator {
//创建A产品家族
publicabstract AbstractProductA createProductA();
//创建B产品家族
publicabstract AbstractProductB createProductB();
}
注意 有N个产品族,在抽象工厂类中就应该有N个创建方法。
如何创建一个产品,则是有具体的实现类来完成的,Creator1和Creator2如代码清单9-15、9-16所示。
产品等级1的实现类
publicclass Creator1 extends AbstractCreator {
//只生产产品等级为1的A产品
publicAbstractProductA createProductA() {
returnnew ProductA1();
}
//只生产产品等级为1的B产品
public AbstractProductBcreateProductB() {
returnnew ProductB1();
}
}
产品等级2的实现类
publicclass Creator2 extends AbstractCreator {
//只生产产品等级为2的A产品
publicAbstractProductA createProductA() {
returnnew ProductA2();
}
//只生产产品等级为2的B产品
public AbstractProductBcreateProductB() {
returnnew ProductB2();
}
}
3:调用
//定义出两个工厂
AbstractCreatorcreator1 = new Creator1();
AbstractCreatorcreator2 = new Creator2();
//产生A1对象
AbstractProductAa1 = creator1.createProductA();
//产生A2对象
AbstractProductAa2 = creator2.createProductA();
//产生B1对象
AbstractProductBb1 = creator1.createProductB();
//产生B2对象
AbstractProductBb2 = creator2.createProductB();
/*
* 然后在这里就可以为所欲为了...
*/
}
4:抽象工厂模式的应用
4.1 抽象工厂模式的优点
封装性,每个产品的实现类不是高层模块要关系的,要关心的是什么?是接口,是抽象,它不关心对象是如何创建出来,这由谁负责呢?工厂类,只要知道工厂类是谁,我就能创建出一个需要的对象,省时省力,优秀设计就应该如此。
产品族内的约束为非公开状态。例如生产男女比例的问题上,猜想女娲娘娘肯定有自己的打算,不能让女盛男衰,否则女性的优点不就体现不出来了吗?那在抽象工厂模式,就应该有这样的一个约束:每生产1个女性,就同时生产出1.2个男性,
这样的生产过程对调用工厂类的高层模块来说是透明的,它不需要知道这个约束,我就是要一个黄色女性产品就可以了,具体的产品族内的约束是在工厂内实现的。
4.2 抽象工厂模式的缺点
抽象工厂模式的最大缺点就是产品族扩展非常困难,为什么这么说呢?我们以通用代码为例,如果要增加一个产品C,也就是说有产品家族由原来的2个,增加到3个,看看我们的程序有多大改动吧!
抽象类AbstractCreator要增加一个方法createProductC(),然后,两个实现类都要修改,想想看,这在项目中的话,还这么让人活!严重违反了开闭原则,而且我们一直说明抽象类和接口是一个契约,
改变契约,所有与契约有关系的代码都要修改,这段代码叫什么?叫“有毒代码”,——只要这段代码有关系,就可能产生侵害的危险!
4.3 抽象工厂模式的使用场景
抽象工厂模式的使用场景定义非常简单:一个对象族(或是一组没有任何关系的对象)都有相同的约束,则可以使用抽象工厂模式,什么意思呢?例如一个文本编辑器和一个图片处理器,都是软件实体,
但是*nix下的文本编辑器和WINDOWS下的文本编辑器虽然功能和界面都相同,但是代码实现是不同的,图片处理器也是类似情况,也就是具有了共同的约束条件:操作系统类型,于是我们可以使用抽象工厂模式,
产生不同操作系统下的编辑器和图片处理器。
4.4 抽象工厂模式的注意实现
在抽象工厂模式的缺点中,我们提到抽象工厂模式的产品族扩展比较困难,但是一定要清楚是产品族扩展困难,而不是产品等级,在该模式下,产品等级是非常容易扩展的,增加一个产品等级,
只要增加一个工厂类负责新增加出来的产品生产任务即可,也就是说横向扩展容易,纵向扩展困难。以人类为例子,产品等级中只要男、女两个性别,现实世界还有一种性别:双性人,即使男人也是女人(俗语就是阴阳人),
那我们要扩展这个产品等级也是非常容易的,增加三个产品类,分别对应不同的肤色,然后再创建一个工厂类,专门负责不同肤色人的双性人的创建任务,完全通过扩展来实现的需求的变更,从这一点上看,抽象工厂模式是符合开闭原则的。
4.5最佳实践
一个模式在什么情况下才能够使用,是很多读者比较困惑的地方,抽象工厂模式是一个简单的模式,使用的场景非常多,大家在软件产品开发过程中,涉及到不同操作系统的时候,都可以考虑使用抽象工厂模式
,例如一个应用,需要在三个不同平台上运行:Windows、Linux、Android(Google发布的智能终端操作系统)上运行,你会怎么设计?分别设计三套不同的应用?非也非也,通过抽象工厂模式屏蔽掉操作系统对应用的影响
。三个不同操作系统上的软件功能、应用逻辑、UI都应该是非常类似,唯一不同的是调用不同的工厂方法,由不同的产品类去处理与操作系统交互的信息。
4:单例模式
单例模式:限制而不是创建,但它任何其他的创建模式一组,可以保证一个类有且只有一个实例,并提供一个访问它的全局访问点。
1:饿汉模式
public class Singleton
{
private static Singleton singlenton;
public Singleton()
{ }
public static Singleton GetSingleton()
{
if (singlenton == null)
{
singlenton = new Singleton();
}
return singlenton;
}
}
2:lazy模式
public class Singleton
{
private static Singleton singlenton;
private static object obj=new object();
public Singleton()
{ }
public static Singleton GetSingleton()
{
if (singlenton == null)
{
lock(obj)
{
singlenton = new Singleton();
}
}
return singlenton;
}
}
调用:
Singleton tance = null;
private void button2_Click(objectsender, EventArgs e)
{
try
{
tance = new Singleton();
}
catch (Exception exp)
{
MessageBox.Show(exp.ToString());
}
}
3:单例模式的特点
单例类只能有一个实例。
单例类必须自己创建自己的唯一实例。
单例类必须给所有其它对象提供这一实例。
4:单例模式应用
每台计算机可以有若干个打印机,但只能有一个PrinterSpooler,避免两个打印作业同时输出到打印机。
一个具有自动编号主键的表可以有多个用户同时使用,但数据库中只能有一个地方分配下一个主键编号。否则会出现主键重复。
5: 生成器模式
///<summary>
/// 建造者模式的演变
/// 省略了指挥者角色和抽象建造者角色
/// 此时具体建造者角色扮演了指挥者和建造者两个角色
/// </summary>
public class Builder
{
// 具体建造者角色的代码
private Product product = newProduct();
public void BuildPartA()
{
product.Add("PartA");
}
public void BuildPartB()
{
product.Add("PartB");
}
public Product GetProduct()
{
return product;
}
// 指挥者角色的代码
public void Construct()
{
BuildPartA();
BuildPartB();
}
}
/// <summary>
/// 产品类
/// </summary>
public class Product
{
// 产品组件集合
private IList<string> parts = newList<string>();
// 把单个组件添加到产品组件集合中
public void Add(string part)
{
parts.Add(part);
}
public void Show()
{
Console.WriteLine("产品开始在组装.......");
foreach (string part in parts)
{
Console.WriteLine("组件" +part + "已装好");
}
Console.WriteLine("产品组装完成");
}
}
// 此时客户端也要做相应调整
class Client
{
private static Builder builder;
static void Main(string[] args)
{
builder = new Builder();
builder.Construct();
Product product =builder.GetProduct();
product.Show();
Console.Read();
}
}
总结:Construct方法,用来调用创建每个组件的方法来完成整个产品的组装
其他的模式在完善中。。。。。。
第四章 多线程
1:线程
l 多线程基础
多线程:线程是程序中一个单一的顺序控制流程.在单个程序中同时运行多个线程完成不同的工作,称为多线程。如果某个线程进行一次长延迟操作,处理器就切换到另一个线程执行。这样,多个线程的并行(并发)执行隐藏了长延迟,
提高了处理器资源利用率,从而提高了整体性能。多线程是为了同步完成多项任务,不是为了提高运行效率,而是为了提高资源使用效率来提高系统的效率。
一、进程与线程
进程,是操作系统进行资源调度和分配的基本单位。是由进程控制块、程序段、数据段三部分组成。一个进程可以包含若干线程(Thread),线程可以帮助应用程序同时做几件事(比如一个线程向磁盘写入文件,另一个则接收用户的按键操作并及时做出反应,互相不干扰),在程序被运行后中,系统首先要做的就是为该程序进程建立一个默认线程,
然后程序可以根据需要自行添加或删除相关的线程。它是可并发执行的程序。在一个数据集合上的运行过程,是系统进行资源分配和调度的一个独立单位,也是称活动、路径或任务,它有两方面性质:活动性、并发性。进程可以划分为运行、阻塞、就绪三种状态,并随一定条件而相互转化:就绪--运行,运行--阻塞,阻塞--就绪。
线程(thread),线程是CPU调度和执行的最小单位。有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,
但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程也有就绪、阻塞和运行三种基本状态。
主线程,进程创建时,默认创建一个线程,这个线程就是主线程。主线程是产生其他子线程的线程,同时,主线程必须是最后一个结束执行的线程,它完成各种关闭其他子线程的操作。尽管主线程是程序开始时自动创建的,它也可以通过Thead类对象来控制,通过调用CurrentThread方法获得当前线程的引用
多线程的优势:进程有独立的地址空间,同一进程内的线程共享进程的地址空间。启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。
二、多线程优点
1、提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种尴尬的情况。
2、使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。
3、改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。。
多线程尽管优势明显,但是线程并发冲突、同步以及管理跟踪,可能给系统带来很多不确定性,这些必须引起足够重视。
废话不多说开始我们的多线程之旅。
三、多线程的应用场合:
简单总结了一下,一般有两种情况:
1)多个线程,完成同类任务,提高并发性能
2)一个任务有多个独立的步骤,多个线程并发执行各子任务,提高任务处理效率
3)不阻断主线程,实现即时响应,由后台线程完成特定操作
五、Thread
创建并控制线程,设置其优先级并获取其状态。
常用方法:
Start()
导致操作系统将当前实例的状态更改为 ThreadState.Running。
一旦线程处于 ThreadState.Running 状态,操作系统就可以安排其执行。线程从方法的第一行(由提供给线程构造函数的 ThreadStart 或 ParameterizedThreadStart 委托表示)开始执行。线程一旦终止,它就无法通过再次调用 Start 来重新启动。
Thread.Sleep()
调用 Thread.Sleep 方法会导致当前线程立即阻止,阻止时间的长度等于传递给 Thread.Sleep 的毫秒数,这样,就会将其时间片中剩余的部分让与另一个线程。一个线程不能针对另一个线程调用 Thread.Sleep。
Interrupt()
中断处于 WaitSleepJoin 线程状态的线程。
Suspend和Resume(已过时)
挂起和继续
在 .NET Framework 2.0 版中,Thread.Suspend 和 Thread.Resume 方法已标记为过时,并将从未来版本中移除。
Abort()
方法用于永久地停止托管线程。一旦线程被中止,它将无法重新启动。
Join()
阻塞调用线程,直到某个线程终止时为止。
ThreadPriority(优先级)
指定 Thread 的调度优先级。
ThreadPriority 定义一组线程优先级的所有可能值。线程优先级指定一个线程相对于另一个线程的相对优先级。
每个线程都有一个分配的优先级。在运行库内创建的线程最初被分配 Normal 优先级,而在运行库外创建的线程在进入运行库时将保留其先前的优先级。可以通过访问线程的 Priority 属性来获取和设置其优先级。
根据线程的优先级调度线程的执行。用于确定线程执行顺序的调度算法随操作系统的不同而不同。操作系统也可以在用户界面的焦点在前台和后台之间移动时动态地调整线程的优先级。
一个线程的优先级不影响该线程的状态;该线程的状态在操作系统可以调度该线程之前必须为 Running。
六:创建线程方式
1: /// <summary>
/// 不带参数的委托
/// </summary>
Thread thread = new Thread(new ThreadStart(ThreadCallBack));
thread.Start();
2: /// <summary>
/// 带参数的委托
/// </summary>
public void CreateThreadWithParamThreadStart()
{
Thread thread = new Thread(newParameterizedThreadStart(ThreadCallBackWithParam));
Object param = null;
thread.Start(param);
}
3: /// <summary>
/// 匿名函数
/// </summary>
public void CreateThreadWithAnonymousFunction()
{
Thread thread = new Thread(delegate()
{
Console.WriteLine("进入子线程1");
for (int i = 1; i < 4; ++i)
{
Thread.Sleep(50);
Console.WriteLine("\t+++++++子线程1+++++++++");
}
Console.WriteLine("退出子线程1");
});
thread.Start();
}
4:/// <summary>
/// 直接赋值委托
/// </summary>
public void CreateThreadWithCallBack()
{
Thread _hThread = new Thread(ThreadCallBack);
_hThread.Start();
}
七:前台线程和后台线程
.Net的公用语言运行时(CommonLanguage Runtime,CLR)能区分两种不同类型的线程:前台线程和后台线程。这两者的区别就是:应用程序必须运行完所有的前台线程才可以退出;而对于后台线程,应用程序则可以不考虑其是否已经运行完毕而直接退出,所有的后台线程在应用程序退出时都会自动结束。
一个线程是前台线程还是后台线程可由它的IsBackground属性来决定。这个属性是可读又可写的。它的默认值为false,即意味着一个线程默认为前台线程。
前台和后台线程的使用原则
既然前台线程和后台线程有这种差别,那么我们怎么知道该如何设置一个线程的IsBackground属性呢?下面是一些基本的原则:对于一些在后台运行的线程,当程序结束时这些线程没有必要继续运行了,那么这些线程就应该设置为后台线程。比如一个程序启动了一个进行大量运算的线程,可是只要程序一旦结束,那个线程就失去了继续存在的意义,
那么那个线程就该是作为后台线程的。而对于一些服务于用户界面的线程往往是要设置为前台线程的,
因为即使程序的主线程结束了,其他的用户界面的线程很可能要继续存在来显示相关的信息,所以不能立即终止它们。这里我只是给出了一些原则,具体到实际的运用往往需要编程者的进一步仔细斟酌。
2:线程同步
一:线程同步方式
线程同步有:临界区、互斥区、事件、信号量四种方式
临界区(Critical Section)、互斥量(Mutex)、信号量(Semaphore)、事件(Event)的区别
1、临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。在任意时刻只允许一个线程对共享资源进行访问,如果有多个线程试图访问公共资源,那么在有一个线程进入后,其他试图访问公共资源的线程将被挂起,并一直等到进入临界区的线程离开,临界区在被释放后,其他线程才可以抢占。
2、互斥量:采用互斥对象机制。只有拥有互斥对象的线程才有访问公共资源的权限,因为互斥对象只有一个,所以能保证公共资源不会同时被多个线程访问。互斥不仅能实现同一应用程序的公共资源安全共享,还能实现不同应用程序的公共资源安全共享
3、信号量:它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目
4、事件: 通过通知操作的方式来保持线程的同步,还可以方便实现对多个线程的优先级比较的操作
二:c#中常见线程同步方法
1:Interlocked
为多个线程共享的变量提供原子操作。
根据经验,那些需要在多线程情况下被保护的资源通常是整型值,且这些整型值在多线程下最常见的操作就是递增、递减或相加操作。Interlocked类提供了一个专门的机制用于完成这些特定的操作。这个类提供了Increment、Decrement、Add静态方法用于对int或long型变量的递增、递减或相加操作。
此类的方法可以防止可能在下列情况发生的错误:计划程序在某个线程正在更新可由其他线程访问的变量时切换上下文;或者当两个线程在不同的处理器上并发执行时。此类的成员不引发异常。
Increment 和 Decrement 方法递增或递减变量并将结果值存储在单个操作中。 在大多数计算机上,增加变量操作不是一个原子操作,需要执行下列步骤:
1)将实例变量中的值加载到寄存器中。
2)增加或减少该值。
3)在实例变量中存储该值。
如果不使用 Increment 和 Decrement,线程会在执行完前两个步骤后被抢先。然后由另一个线程执行所有三个步骤。当第一个线程重新开始执行时,它覆盖实例变量中的值,造成第二个线程执行增减操作的结果丢失。
Exchange 方法自动交换指定变量的值。CompareExchange 方法组合了两个操作:比较两个值以及根据比较的结果将第三个值存储在其中一个变量中。比较和交换操作按原子操作执行。
使用://检查大引进是否在使用,如果原始值为0,则为未使用,可以进行打印,否则不能打印,继续等待
if (0 == Interlocked.Exchange(ref UsingPrinter, 1))
{
//操作
//释放打印机
Interlocked.Exchange(refUsingPrinter, 0);
}
2:Lock 关键字
lock 关键字将语句块标记为临界区,方法是获取给定对象的互斥锁,执行语句,然后释放该锁。
lock 确保当一个线程位于代码的临界区时,另一个线程不进入临界区。如果其他线程试图进入锁定的代码,则它将一直等待(即被阻止),直到该对象被释放。
public void Function()
{
System.Object locker= new System.Object();
lock(locker)
{
// Access thread-sensitive resources.
}
}
lock 调用块开始位置的 Enter 和块结束位置的 Exit。
提供给 lock 关键字的参数必须为基于引用类型的对象,该对象用来定义锁的范围。在上例中,锁的范围限定为此函数,因为函数外不存在任何对该对象的引用。严格地说,提供给 lock 的对象只是用来唯一地标识由多个线程共享的资源,所以它可以是任意类实例。然而,实际上,此对象通常表示需要进行线程同步的资源。例如,如果一个容器对象将被多个线程使用,则可以将该容器传递给 lock,而 lock 后面的同步代码块将访问该容器。
只要其他线程在访问该容器前先锁定该容器,则对该对象的访问将是安全同步的。通常,最好避免锁定 public 类型或锁定不受应用程序控制的对象实例,例如,如果该实例可以被公开访问,则 lock(this) 可能会有问题,因为不受控制的代码也可能会锁定该对象。这可能导致死锁,即两个或更多个线程等待释放同一对象。出于同样的原因,锁定公共数据类型(相比于对象)也可能导致问题。锁定字符串尤其危险,因为字符串被公共语言运行库 (CLR)“暂留”。
这意味着整个程序中任何给定字符串都只有一个实例,就是这同一个对象表示了所有运行的应用程序域的所有线程中的该文本。因此,只要在应用程序进程中的任何位置处具有相同内容的字符串上放置了锁,就将锁定应用程序中该字符串的所有实例。因此,最好锁定不会被暂留的私有或受保护成员。某些类提供专门用于锁定的成员。例如,Array 类型提供 SyncRoot。许多集合类型也提供 SyncRoot。
常见的结构 lock (this)、lock (typeof (MyType)) 和 lock("myLock") 违反此准则:
1)如果实例可以被公共访问,将出现 lock(this) 问题。
2)如果 MyType 可以被公共访问,将出现lock (typeof (MyType)) 问题。
3)由于进程中使用同一字符串的任何其他代码将共享同一个锁,所以出现 lock(“myLock”) 问题。
最好的做法:
1、如果两个操作是相互影响的,比如读写一个文件,只能允许一个执行,则锁对象应该相同。
因为lock隐含是Monitor, Monitor类通过向单个线程授予对象锁来控制对对象的访问。对象锁提供限制访问代码块(通常称为临界区)的能力。当一个线程拥有对象的锁时,其他任何线程都不能获取该锁。还可以使用 Monitor 来确保不会允许其他任何线程访问正在由锁的所有者执行的应用程序代码节,除非另一个线程正在使用其他的锁定对象执行该代码。
2、如果两个操作是互不相干的,则其锁对象应该不同,如若采用同一个锁,将直接影响其它操作的执行。
在我们开发过程中,经常为了省事,而只创建一个锁对象,还有的在基类创建一个锁,由子类共用,这是在系统架构中采用工厂模式,经常出现的误区。如果是互不相干的操作,一个操作的执行必须等待另一个操作结束之后才能执行,必然受到了该锁的影响,大大降低了系统的性能,有时候会造成死锁。
3、lock本身也有系统损耗。
lock本身也需要利用资源,所以不必要的锁会降低系统的性能。在这个试验里,加了锁和不加锁,执行的结果不一样,加了锁输出的结果会缩短。这个你自己也可以写一个小例子进行测试。所以使用锁一定要慎重,不能滥用。
3: Monitor关键字
与 lock 关键字类似,监视器防止多个线程同时执行代码块。Enter 方法允许一个且仅一个线程继续执行后面的语句;其他所有线程都将被阻止,直到执行语句的线程调用 Exit。这与使用 lock 关键字一样。事实上,lock 关键字就是用 Monitor 类来实现的
/// <summary>
/// 使用打印机进行打印
/// </summary>
private static void UsePrinterWithMonitor()
{
System.Threading.Monitor.Enter(UsingPrinterLocker);
try
{
Console.WriteLine("{0}acquired the lock", Thread.CurrentThread.Name);
//模拟打印操作
Thread.Sleep(500);
Console.WriteLine("{0}exiting lock", Thread.CurrentThread.Name);
}
finally
{
System.Threading.Monitor.Exit(UsingPrinterLocker);
}
}
使用 lock 关键字通常比直接使用 Monitor 类更可取,一方面是因为 lock 更简洁,另一方面是因为 lock 确保了即使受保护的代码引发异常,也可以释放基础监视器。这是通过 finally 关键字来实现的,无论是否引发异常它都执行关联的代码块。
4:同步事件和等待句柄
使用锁或监视器对于防止同时执行区分线程的代码块很有用,但是这些构造不允许一个线程向另一个线程传达事件。这需要“同步事件”,它是有两个状态(终止和非终止)的对象,可以用来激活和挂起线程。让线程等待非终止的同步事件可以将线程挂起,将事件状态更改为终止可以将线程激活。如果线程试图等待已经终止的事件,则线程将继续执行,而不会延迟。
同步事件有两种:AutoResetEvent和 ManualResetEvent。它们之间唯一的不同在于,无论何时,只要 AutoResetEvent 激活线程,它的状态将自动从终止变为非终止。相反,ManualResetEvent 允许它的终止状态激活任意多个线程,只有当它的 Reset 方法被调用时才还原到非终止状态。
等待句柄,可以通过调用一种等待方法,如 WaitOne、WaitAny 或 WaitAll,让线程等待事件。System.Threading.WaitHandle.WaitOne 使线程一直等待,直到单个事件变为终止状态;System.Threading.WaitHandle.WaitAny阻止线程,直到一个或多个指示的事件变为终止状态;System.Threading.WaitHandle.WaitAll 阻止线程,直到所有指示的事件都变为终止状态。当调用事件的 Set 方法时,事件将变为终止状态。
AutoResetEvent 允许线程通过发信号互相通信。通常,当线程需要独占访问资源时使用该类。线程通过调用 AutoResetEvent 上的 WaitOne 来等待信号。如果 AutoResetEvent 为非终止状态,则线程会被阻止,并等待当前控制资源的线程通过调用 Set 来通知资源可用。调用 Set 向 AutoResetEvent 发信号以释放等待线程。 AutoResetEvent 将保持终止状态,直到一个正在等待的线程被释放,然后自动返回非终止状态。如果没有任何线程在等待,则状态将无限期地保持为终止状态。
如果当 AutoResetEvent 为终止状态时线程调用 WaitOne,则线程不会被阻止。 AutoResetEvent 将立即释放线程并返回到非终止状态。
可以通过将一个布尔值传递给构造函数来控制 AutoResetEvent 的初始状态:如果初始状态为终止状态,则为 true;否则为 false。
AutoResetEvent 也可以同staticWaitAll 和 WaitAny 方法一起使用。
private AutoResetEvent resetEvent = newAutoResetEvent(false);
resetEvent.Reset();
//等待空闲出来
resetEvent.WaitOne();
5:Mutex对象
mutex 与监视器类似;它防止多个线程在某一时间同时执行某个代码块。事实上,名称“mutex”是术语“互相排斥 (mutually exclusive)”的简写形式。然而与监视器不同的是,mutex 可以用来使跨进程的线程同步。mutex 由 Mutex 类表示。当用于进程间同步时,mutex 称为“命名 mutex”,因为它将用于另一个应用程序,因此它不能通过全局变量或静态变量共享。必须给它指定一个名称,才能使两个应用程序访问同一个 mutex 对象。
尽管 mutex 可以用于进程内的线程同步,但是使用 Monitor 通常更为可取,因为监视器是专门为 .NET Framework 而设计的,因而它可以更好地利用资源。相比之下,Mutex 类是 Win32 构造的包装。尽管 mutex 比监视器更为强大,但是相对于 Monitor 类,它所需要的互操作转换更消耗计算资源。
本地 mutex 和系统 mutex
Mutex 分两种类型:本地 mutex 和命名系统 mutex。如果使用接受名称的构造函数创建了 Mutex 对象,那么该对象将与具有该名称的操作系统对象相关联。命名的系统 mutex 在整个操作系统中都可见,并且可用于同步进程活动。您可以创建多个 Mutex 对象来表示同一命名系统 mutex,而且您可以使用 OpenExisting 方法打开现有的命名系统 mutex。
本地 mutex 仅存在于进程当中。进程中引用本地 Mutex 对象的任意线程都可以使用本地 mutex。每个 Mutex 对象都是一个单独的本地 mutex。
在本地Mutex中,用法与Monitor基本一致
6:读取器/编写器锁
ReaderWriterLockSlim 类允许多个线程同时读取一个资源,但在向该资源写入时要求线程等待以获得独占锁。
可以在应用程序中使用 ReaderWriterLockSlim,以便在访问一个共享资源的线程之间提供协调同步。获得的锁是针对 ReaderWriterLockSlim 本身的。
设计您应用程序的结构,让读取和写入操作的时间尽可能最短。因为写入锁是排他的,所以长时间的写入操作会直接影响吞吐量。长时间的读取操作会阻止处于等待状态的编写器,并且,如果至少有一个线程在等待写入访问,则请求读取访问的线程也将被阻止。
三:多线程思考
1:如何能写出线程安全的代码
在OOP中,程序员使用的无非是:变量、对象(属性、方法)、类型等等。
1)变量
变量包括值类型和引用类型。
值类型是线程安全的,但是如果作为对象的属性,值类型就被附加到对象上,需要参考对象的线程安全性。
引用类型,这里要注意的是,对于引用对象,他包括了引用和对象实例两部分,实例需要通过对其存储位置的引用来访问,对于
private Object o = new Object(),
其实可以分解为两句话:
private Object o;
o = new Object();
其中private Object o是定义了对象的引用,也就是记录对象实例的指针,而不是对象本身。这个引用存储于堆栈中,占用4个字节;当没有使用o = new Object()时,引用本身的值为null,也就是不指向任何有效位置;当o = new Object()后,才真正根据对象的大小,在托管堆中分配空间给对象实例,然后将实例的指针位置赋值给前面的引用。这才完成一个对象的实例化。
引用类型的安全性,在于:可以由多个引用,同时指向一个内存地址。如果一个引用被修改,另一个也会修改。
2)对象
对象是类型的实例
在创建对象时,会单独有内存区域存储对象的属性和方法。所以,一个类型的多个实例,在执行时,只要没有静态变量的参与,应该都是线程安全的。
这跟我们调试状态下,是不一样的。调试状态下,如果多个线程都创建某实例的对象,每个对象都调用自身方法,在调试是,会发现是访问的同一个代码,多个线程是有冲突的。但是,真正的运行环境是线程安全的。
3)类型
已经讲了类的实例--对象的多线程安全性问题。这里只讨论类型的静态变量和静态方法。
当静态类被访问的时候,CLR会调用类的静态构造器(类型构造器),创建静态类的类型对象,CLR希望确保每个应用程序域内只执行一次类型构造器,为了做到这一点,在调用类型构造器时,
CLR会为静态类加一个互斥的线程同步锁,因此,如果多个线程试图同时调用某个类型的静态构造器时,那么只有一个线程可以获得对静态类的访问权,其他的线程都被阻塞。第一个线程执行完类型构造器的代码并释放构造器之后,
其他阻塞的线程被唤醒,然后发现构造器被执行过,因此,这些线程不再执行构造器,只是从构造器简单的返回。如果再一次调用这些方法,CLR就会意识到类型构造器被执行过,从而不会在被调用。
调用类中的静态方法,或者访问类中的静态成员变量,过程同上,所以说静态类是线程安全的。
四:集合线程安全吗?
常用的集合类型有List、Dictionary、HashTable、HashMap等。在编码中,集合应用很广泛中,常用集合来自定义Cache,这时候必须考虑线程同步问题。
默认情况下集合不是线程安全的。在System.Collections 命名空间中只有几个类提供Synchronize方法,该方法能够超越集合创建线程安全包装。但是,System.Collections命名空间中的所有类都提供SyncRoot属性,可供派生类创建自己的线程安全包装。还提供了IsSynchronized属性以确定集合是否是线程安全的。但是ICollection泛型接口中不提供同步功能,非泛型接口支持这个功能。
Dictionary(MSDN解释)
此类型的公共静态(在Visual Basic 中为 Shared)成员是线程安全的。但不保证所有实例成员都是线程安全的。
只要不修改该集合,Dictionary<TKey,TValue> 就可以同时支持多个阅读器。即便如此,从头到尾对一个集合进行枚举本质上并不是一个线程安全的过程。当出现枚举与写访问互相争用这种极少发生的情况时,必须在整个枚举过程中锁定集合。若允许多个线程对集合执行读写操作,您必须实现自己的同步。
很多集合类型都和Dictionary类似。默认情况下是线程不安全的。当然微软也提供了线程安全的Hashtable.
HashTable
Hashtable 是线程安全的,可由多个读取器线程和一个写入线程使用。多线程使用时,如果只有一个线程执行写入(更新)操作,则它是线程安全的,从而允许进行无锁定的读取(若编写器序列化为 Hashtable)。若要支持多个编写器,如果没有任何线程在读取 Hashtable 对象,则对 Hashtable 的所有操作都必须通过 Synchronized 方法返回的包装完成。
从头到尾对一个集合进行枚举本质上并不是一个线程安全的过程。即使某个集合已同步,其他线程仍可以修改该集合,这会导致枚举数引发异常。若要在枚举过程中保证线程安全,可以在整个枚举过程中锁定集合,或者捕捉由于其他线程进行的更改而引发的异常。
五:如何使用多线程
程可以大大提高应用程序的可用性和性能,但是多线程也给我们带来一些新的挑战,要不要使用多线程,如何使用多线程,需要根据实际情况而定。
1)复杂度
使用多线程,可能使得应用程序复杂度明显提高,特别是要处理线程同步和死锁问题。需要仔细地评估应该在何处使用多线程和如何使用多线程,这样就可以获得最大的好处,而无需创建不必要的复杂并难于调试的应用程序。
2)数量
线程不易过多,线程的数量与服务器配置(多核、多处理器)、业务处理具体过程,都有直接关系。线程量过少,不能充分发挥服务器的处理能力,也不能有效改善事务的处理效率。线程量过多,需要花费大量的时间来进行线程控制,最后得不偿失。可以根据实际情况,通过检验测试,设定一个特定的合理的范围。
3)同步和异步调用之间的选择
应用程序既可以进行同步调用,也可以进行异步调用。同步调用在继续之前等待响应或返回值。如果不允许调用继续,就说调用被阻塞了。异步或非阻塞调用不等待响应。异步调用是通过使用单独的线程执行的。原始线程启动异步调用,异步调用使用另一个线程执行请求,而与此同时原始的线程继续处理。
4)前台线程和后台线程之间的选择
.NET Framework 中的所有线程都被指定为前台线程或后台线程。这两种线程唯一的区别是— 后台线程不会阻止进程终止。在属于一个进程的所有前台线程终止之后,公共语言运行库 (CLR) 就会结束进程,从而终止仍在运行的任何后台线程。
在默认情况下,通过创建并启动新的 Thread 对象生成的所有线程都是前台线程,而从非托管代码进入托管执行环境中的所有线程都标记为后台线程。然而,通过修改 Thread.IsBackground 属性,可以指定一个线程是前台线程还是后台线程。通过将 Thread.IsBackground 设置为 true,可以将一个线程指定为后台线程;通过将 Thread.IsBackground 设置为 false,可以将一个线程指定为前台线程。
在大多数应用程序中,您会选择将不同的线程设置成前台线程或后台线程。通常,应该将被动侦听活动的线程设置为后台线程,而将负责发送数据的线程设置为前台线程,这样,在所有的数据发送完毕之前该线程不会被终止。只有在确认线程被系统随意终止没有不利影响时,才应该使用后台线程。如果线程正在执行必须完成的敏感操作或事务操作,或者需要控制关闭线程的方式以便释放重要资源,则使用前台线程。
六:何时使用线程池(ThreadPool)
到现在为止,您可能会认识到许多应用程序都会从多线程处理中受益。然而,线程管理并不仅仅是每次想要执行一个不同的任务就创建一个新线程的问题。有太多的线程可能会使得应用程序耗费一些不必要的系统资源,特别是,如果有大量短期运行的操作,而所有这些操作都运行在单独线程上。另外,显式地管理大量的线程可能是非常复杂的。
线程池化技术通过给应用程序提供由系统管理的辅助线程池解决了这些问题,从而使得您可以将注意力集中在应用程序任务上而不是线程管理上。
在需要时,可以由应用程序将线程添加到线程池中。当 CLR 最初启动时,线程池没有包含额外的线程。然而,当应用程序请求线程时,它们就会被动态创建并存储在该池中。如果线程在一段时间内没有使用,这些线程就可能会被处置,因此线程池是根据应用程序的要求缩小或扩大的。
注意:每个进程都创建一个线程池,因此,如果您在同一个进程内运行几个应用程序域,则一个应用程序域中的错误可能会影响相同进程内的其他应用程序域,因为它们都使用相同的线程池。
线程池由两种类型的线程组成:
辅助线程。辅助线程是标准系统池的一部分。它们是由 .NET Framework 管理的标准线程,大多数功能都在它们上面执行。
完成端口线程.这种线程用于异步 I/O 操作(通过使用 IOCompletionPorts API)
对于每个计算机处理器,线程池都默认包含 25 个线程。如果所有的 25 个线程都在被使用,则附加的请求将排入队列,直到有一个线程变得可用为止。每个线程都使用默认堆栈大小,并按默认的优先级运行。
下面代码示例说明了线程池的使用。
private void ThreadPoolExample()
{
WaitCallback callback = new WaitCallback( ThreadProc );
ThreadPool.QueueUserWorkItem( callback );
}
在前面的代码中,首先创建一个委托来引用您想要在辅助线程中执行的代码。.NET Framework 定义了 WaitCallback 委托,该委托引用的方法接受一个对象参数并且没有返回值。下面的方法实现您想要执行的代码。
private void ThreadProc( Object stateInfo )
{
// Do something on worker thread.
}
可以将单个对象参数传递给 ThreadProc 方法,方法是将其指定为 QueueUserWorkItem 方法调用中的第二个参数。在前面的示例中,没有给 ThreadProc 方法传递参数,因此 stateInfo 参数为空。
在下面的情况下,使用 ThreadPool 类:
有大量小的独立任务要在后台执行。
不需要对用来执行任务的线程进行精细控制。
Thread是显示来管理线程。只要有可能,就应该使用ThreadPool 类来创建线程。
在下面的情况下,使用 Thread 对象:
需要具有特定优先级的任务。
有可能运行很长时间的任务(这样可能阻塞其他任务)。
需要确保只有一个线程可以访问特定的程序集。
需要有与线程相关的稳定标识。
第五章 异步线程
1: 什么是异步?
异步操作通常用于执行完成时间可能较长的任务,如打开大文件、连接远程计算机或查询数据库。异步操作在主应用程序线程以外的线程中执行。应用程序调用方法异步执行某个操作时,应用程序可在异步方法执行其任务时继续执行。
2: 同步与异步的区别
同步(Synchronous):在执行某个操作时,应用程序必须等待该操作执行完成后才能继续执行。
异步(Asynchronous):在执行某个操作时,应用程序可在异步操作执行时继续执行。实质:异步操作,启动了新的线程,主线程与方法线程并行执行。
3: 异步和多线程的区别
我们已经知道,异步的实质是开启了新的线程。它与多线程的区别是什么呢?
简单的说就是:异步线程是由线程池负责管理,而多线程,我们可以自己控制,当然在多线程中我们也可以使用线程池。
就拿网络扒虫而言,如果使用异步模式去实现,它使用线程池进行管理。异步操作执行时,会将操作丢给线程池中的某个工作线程来完成。当开始I/O操作的时候,异步会将工作线程还给线程池,这意味着获取网页的工作不会再占用任何CPU资源了。直到异步完成,即获取网页完毕,异步才会通过回调的方式通知线程池。可见,异步模式借助于线程池,极大地节约了CPU的资源。
注:DMA(Direct Memory Access)直接内存存取,顾名思义DMA功能就是让设备可以绕过处理器,直接由内存来读取资料。通过直接内存访问的数据交换几乎可以不损耗CPU的资源。在硬件中,硬盘、网卡、声卡、显卡等都有直接内存访问功能。异步编程模型就是让我们充分利用硬件的直接内存访问功能来释放CPU的压力。
两者的应用场景:
计算密集型工作,采用多线程。
IO密集型工作,采用异步机制。
4: 异步应用
.NET Framework 的许多方面都支持异步编程功能,这些方面包括:
1)文件 IO、流 IO、套接字 IO。
2)网络。
3)远程处理信道(HTTP、TCP)和代理。
4)使用 ASP.NET创建的 XML Web services。
5)ASP.NETWeb 窗体。
6)使用MessageQueue 类的消息队列。
.NET Framework 为异步操作提供两种设计模式:
1)使用IAsyncResult 对象的异步操作。
2)使用事件的异步操作。
IAsyncResult 设计模式允许多种编程模型,但更加复杂不易学习,可提供大多数应用程序都不要求的灵活性。可能的话,类库设计者应使用事件驱动模型实现异步方法。在某些情况下,库设计者还应实现基于 IAsyncResult 的模型。
使用 IAsyncResult 设计模式的异步操作是通过名为 Begin操作名称和End操作名称的两个方法来实现的,这两个方法分别开始和结束异步操作操作名称。例如,FileStream 类提供BeginRead 和 EndRead方法来从文件异步读取字节。这两个方法实现了 Read 方法的异步版本。在调用 Begin操作名称后,应用程序可以继续在调用线程上执行指令,同时异步操作在另一个线程上执行。每次调用 Begin操作名称 时,应用程序还应调用 End操作名称来获取操作的结果。Begin操作名称 方法开始异步操作操作名称并返回一个实现 IAsyncResult 接口的对象。 .NET Framework 允许您异步调用任何方法。定义与您需要调用的方法具有相同签名的委托;公共语言运行库将自动为该委托定义具有适当签名的 BeginInvoke 和EndInvoke 方法。
IAsyncResult 对象存储有关异步操作的信息。下表提供了有关异步操作的信息。
名称 |
说明 |
AsyncState |
获取用户定义的对象,它限定或包含关于异步操作的信息。 |
AsyncWaitHandle |
获取用于等待异步操作完成的 WaitHandle。 |
CompletedSynchronously |
获取一个值,该值指示异步操作是否同步完成。 |
IsCompleted |
获取一个值,该值指示异步操作是否已完 |
5: 应用实例
案例1-读取文件
通常读取文件是一个比较耗时的工作,特别是读取大文件的时候,常见的上传和下载。但是我们又不想让用户一直等待,用户同样可以进行其他操作,可以使得系统有良好的交互性。这里我们写了同步调用和异步调用来进行比较说明。
读取文件类
using System;
using System.IO;
using System.Threading;
namespace AsynSample
{
class FileReader
{
/// <summary>
///缓存池
/// </summary>
private byte[] Buffer { get; set; }
/// <summary>
///缓存区大小
/// </summary>
public int BufferSize { get; set; }
public FileReader(int bufferSize)
{
this.BufferSize = bufferSize;
this.Buffer = new byte[BufferSize];
}
/// <summary>
///同步读取文件
/// </summary>
/// <param name="path">文件路径</param>
public void SynsReadFile(string path)
{
Console.WriteLine("同步读取文件 begin");
using (FileStream fs = new FileStream(path, FileMode.Open))
{
fs.Read(Buffer, 0, BufferSize);
string output = System.Text.Encoding.UTF8.GetString(Buffer);
Console.WriteLine("读取的文件信息:{0}",output);
}
Console.WriteLine("同步读取文件 end");
}
/// <summary>
///异步读取文件
/// </summary>
/// <param name="path"></param>
public void AsynReadFile(string path)
{
Console.WriteLine("异步读取文件 begin");
//执行Endread时报错,fs已经释放,注意在异步中不能使用释放需要的资源
//using (FileStream fs = new FileStream(path, FileMode.Open))
//{
// Buffer = new byte[BufferSize];
// fs.BeginRead(Buffer, 0, BufferSize, AsyncReadCallback, fs);
//}
if (File.Exists(path))
{
FileStream fs = new FileStream(path, FileMode.Open);
fs.BeginRead(Buffer, 0, BufferSize, AsyncReadCallback, fs);
}
else
{
Console.WriteLine("该文件不存在");
}
}
/// <summary>
///
/// </summary>
/// <param name="ar"></param>
void AsyncReadCallback(IAsyncResult ar)
{
FileStream stream = ar.AsyncState as FileStream;
if (stream != null)
{
Thread.Sleep(1000);
//读取结束
stream.EndRead(ar);
stream.Close();
string output = System.Text.Encoding.UTF8.GetString(this.Buffer);
Console.WriteLine("读取的文件信息:{0}", output);
}
}
}
}
测试用例
using System;
using System.Threading;
namespace AsynSample
{
class Program
{
static void Main(string[] args)
{
FileReader reader = new FileReader(1024);
//改为自己的文件路径
string path = "C:\\Windows\\DAI.log";
Console.WriteLine("开始读取文件了...");
//reader.SynsReadFile(path);
reader.AsynReadFile(path);
Console.WriteLine("我这里还有一大滩事呢.");
DoSomething();
Console.WriteLine("终于完事了,输入任意键,歇着!");
Console.ReadKey();
}
/// <summary>
///
/// </summary>
static void DoSomething()
{
Thread.Sleep(1000);
for (int i = 0; i < 10000; i++)
{
if (i % 888 == 0)
{
Console.WriteLine("888的倍数:{0}",i);
}
}
}
}
}
输出结果:
同步输出:
异步输出:
结果分析:
如果是同步读取,在读取时,当前线程读取文件,只能等到读取完毕,才能执行以下的操作
而异步读取,是创建了新的线程,读取文件,而主线程,继续执行。我们可以开启任务管理器来进行监视。
案例二--基于委托的异步操作
系统自带一些类具有异步调用方式,如何使得自定义对象也具有异步功能呢?
我们可以借助委托来轻松实现异步。
说到BeginInvoke,EndInvoke就不得不停下来看一下委托的本质。为了便于理解委托,我定义一个简单的委托:
public delegate string MyFunc(int num, DateTime dt);
我们再来看一下这个委托在编译后的程序集中是个什么样的:
委托被编译成一个新的类型,拥有BeginInvoke,EndInvoke,Invoke这三个方法。前二个方法的组合使用便可实现异步调用。第三个方法将以同步的方式调用。 其中BeginInvoke方法的最后二个参数用于回调,其它参数则与委托的包装方法的输入参数是匹配的。 EndInvoke的返回值与委托的包装方法的返回值匹配。
异步实现文件下载:
using System;
using System.Text;
namespace AsynSample
{
/// <summary>
///下载委托
/// </summary>
/// <param name="fileName"></param>
public delegate string AysnDownloadDelegate(string fileName);
/// <summary>
///通过委托实现异步调用
/// </summary>
class DownloadFile
{
/// <summary>
///同步下载
/// </summary>
/// <param name="fileName"></param>
public string Downloading(string fileName)
{
string filestr = string.Empty;
Console.WriteLine("下载事件开始执行");
System.Threading.Thread.Sleep(3000);
Random rand = new Random();
StringBuilder builder =new StringBuilder();
int num;
for(int i=0;i<100;i++)
{
num = rand.Next(1000);
builder.Append(i);
}
filestr = builder.ToString();
Console.WriteLine("下载事件执行结束");
return filestr;
}
/// <summary>
///异步下载
/// </summary>
public IAsyncResult BeginDownloading(string fileName)
{
string fileStr = string.Empty;
AysnDownloadDelegate downloadDelegate = new AysnDownloadDelegate(Downloading);
return downloadDelegate.BeginInvoke(fileName, Downloaded, downloadDelegate);
}
/// <summary>
///异步下载完成后事件
/// </summary>
/// <param name="result"></param>
private void Downloaded(IAsyncResult result)
{
AysnDownloadDelegate aysnDelegate = result.AsyncState as AysnDownloadDelegate;
if (aysnDelegate != null)
{
string fileStr = aysnDelegate.EndInvoke(result);
if (!string.IsNullOrEmpty(fileStr))
{
Console.WriteLine("下载文件:{0}", fileStr);
}
else
{
Console.WriteLine("下载数据为空!");
}
}
else
{
Console.WriteLine("下载数据为空!");
}
}
}
}
通过案例,我们发现,使用委托能够很轻易的实现异步。这样,我们就可以自定义自己的异步操作了。
文章来源:http://www.cnblogs.com/yank/p/3239767.html
第六章 线程异常与处理
1: 线程异常分类
1:主线程捕捉子线程的异常
先看一段网上的异常的代码。
class Program
{
static void Main(string[] args)
{
try
{
System.Threading.Thread thread = new System.Threading.Thread(new Program().run);
thread.Start();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
Thread.Sleep(1000);
}
public void run()
{
throw new Exception();
}
}
在线程运行的过程中抛出了异常
那我们该怎么处理这个问题,这个问题产生的原因是什么?
首先需要了解异常的实现机制:异常的实现机制是严重依赖与线程的栈的。每个线程都有一个栈,线程启动后会在栈上安装一些异常处理帧,并形成一个链表的结构,在异常发生时通过该链表可以进行栈回滚,如果你自己没有安装的话,可能会直接跳到链表尾部,那可能是CRT提供的一个默认处理帧,弹出一个对话框提示某某地址内存错误等,然后确认调试,取消关闭。
所以说,线程之间是不可能发生异常处理的交换关系的。
那我们该怎么处理呢?
class Program
{
privatedelegatevoid ExceptionHandler(Exceptionex);
privatestaticExceptionHandlerexception;
privatestaticvoidProcessException(Exception ex)
{
Console.WriteLine(ex.Message);
}
static void Main(string[]args)
{
exception = new ExceptionHandler(ProcessException);
System.Threading.Thread thread =newSystem.Threading.Thread(newProgram().run);
thread.Start();
Thread.Sleep(1000);
Console.Read();
}
public void run()
{
try
{
thrownewException();
}
catch(Exception ex)
{ ProcessException(ex); }
}
}
处理后运行的结果:
看到上图,我们已经捕获的出现的异常。
2:跨线程访问异常
我们可以举一个在c#开发中经常会遇到的问题:
在C# 的应用程序开发中, 我们经常要把UI线程和工作线程分开,防止界面停止响应。 同时我们又需要在工作线程中更新UI界面上的控件
下面的这个例子抛出了异常
private void button1_Click(object sender, EventArgs e)
{
Thread thread1 = new Thread(newParameterizedThreadStart(UpdateLabel));
thread1.Start("更新Label");
}
private voidUpdateLabel(object str)
{
this.label1.Text =str.ToString();
}
运行后抛出异常:
处理这个异常,我们有如下几个办法:
1:禁止编译器对跨线程的访问做检查(不推荐使用)
public Form1()
{
InitializeComponent();
// 加入这行
Control.CheckForIllegalCrossThreadCalls = false;
}
是最简单的办法, 相当于不检查线程之间的冲突,允许各个线程随便乱搞,最后Lable1控件的值是什么就难以预料了
2: 使用delegate和invoke来从其他线程中调用控件
private void button2_Click(object sender, EventArgs e)
{
Thread thread1 = new Thread(newParameterizedThreadStart(UpdateLabel2));
thread1.Start("更新Label");
}
private voidUpdateLabel2(object str)
{
if(label2.InvokeRequired)
{
// 当一个控件的InvokeRequired属性值为真时,说明有一个创建它以外的线程想访问它
// Action<string> actionDelegate = (x)=> { this.label2.Text = //x .ToString(); };
// 或者
// Action<string>actionDelegate = delegate(string txt) //{ this.label2.Text = txt; };
this.label2.Invoke(actionDelegate,str);
}
else
{
this.label2.Text = str.ToString();
}
}
3:使用delegate和BeginInvoke来从其他线程中控制控件
只要把上面的 this.label2.Invoke(actionDelegate,str); 中的 Invoke 改为BeginInvoke方法就可以了
Invoke方法和BeginInvoke方法的区别是
Invoke方法是同步的, 它会等待工作线程完成,
BeginInvoke方法是异步的, 它会另起一个线程去完成工作线程
BackgroundWorker是.NET里面用来执行多线程任务的控件,它允许编程者在一个单独的线程上执行一些操作。耗时的操作(如下载和数据库事务)。用法简单
privatevoid button4_Click(object sender, EventArgs e)
{
using (BackgroundWorker bw = newBackgroundWorker())
{
bw.RunWorkerCompleted += newRunWorkerCompletedEventHandler(bw_RunWorkerCompleted);
bw.DoWork += newDoWorkEventHandler(bw_DoWork);
bw.RunWorkerAsync("Tank");
}
}
void bw_DoWork(object sender,DoWorkEventArgs e)
{
// 这里是后台线程, 是在另一个线程上完成的
// 这里是真正做事的工作线程
// 可以在这里做一些费时的,复杂的操作
Thread.Sleep(5000);
e.Result = e.Argument + "工作线程完成";
}
void bw_RunWorkerCompleted(objectsender, RunWorkerCompletedEventArgs e)
{
//这时后台线程已经完成,并返回了主线程,所以可以直接使用UI控件了
this.label4.Text =e.Result.ToString();
}
第七章 WPF的MVVM框架
1:MVVM理论知识
从上一篇文章中,我们已经知道,WPF技术的主要特点是数据驱动UI,所以在使用WPF技术开发的过程中是以数据为核心的,WPF提供了数据绑定机制,当数据发生变化时,WPF会自动发出通知去更新UI。
我们使用模式,一般是想达到高内聚低耦合。在WPF开发中,经典的编程模式是MVVM,是为WPF量身定做的模式,该模式充分利用了WPF的数据绑定机制,最大限度地降低了Xmal文件和CS文件的耦合度,也就是UI显示和逻辑代码的耦合度,如需要更换界面时,逻辑代码修改很少,甚至不用修改。与WinForm开发相比,我们一般在后置代码中会使用控件的名字来操作控件的属性来更新UI,而在WPF中通常是通过数据绑定来更新UI;在响应用户操作上,WinForm是通过控件的事件来处理,而WPF可以使用命令绑定的方式来处理,耦合度将降低。
我们可以通过下图来直观的理解MVVM模式:
View就是用xaml实现的界面,负责与用户交互,接收用户输入,把数据展现给用户。
ViewModel,一个C#类,负责收集需要绑定的数据和命令,聚合Model对象,通过View类的DataContext属性绑定到View,同时也可以处理一些UI逻辑。
Model,就是系统中的对象,可包含属性和行为。
一般,View对应一个ViewModel,ViewModel可以聚合N个Model,ViewModel可以对应多个View,Model不知道View和ViewModel的存在。
2:MVVM示例讲解
这个示例是为了让大家直观地了解MVVM的编程模式,关于其中用到的数据绑定和命令等知识,在后面的文章会专门讨论。
1:定义NotificationObject类
首先定义NotificationObject类。目的是绑定数据属性。这个类的作用是实现了INotifyPropertyChanged接口。WPF中类要实现这个接口,其属性成员才具备通知UI的能力,数据绑定的知识,后面详细讨论。
using System.ComponentModel;
namespace WpfFirst
{
class NotificationObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, newPropertyChangedEventArgs(propertyName));
}
}
}
}
2:定义DelegateCommand类
目的是绑定命令属性。这个类的作用是实现了ICommand接口,WPF中实现了ICommand接口的类,才能作为命令绑定到UI。命令的知识,后面详细讨论。
using System;
using System.Collections.Generic;
using System.Windows.Input;
namespace WpfFirst
{
class DelegateCommand : ICommand
{
//A method prototype without return value.
public Action<object> ExecuteCommand = null;
//A method prototype return a bool type.
public Func<object, bool> CanExecuteCommand = null;
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
if (CanExecuteCommand != null)
{
return this.CanExecuteCommand(parameter);
}
else
{
return true;
}
}
public void Execute(object parameter)
{
if (this.ExecuteCommand != null) this.ExecuteCommand(parameter);
}
public void RaiseCanExecuteChanged()
{
if (CanExecuteChanged != null)
{
CanExecuteChanged(this,EventArgs.Empty);
}
}
}
}
3:定义Model类
一个属性成员"WPF",它就是数据属性,的有通知功能,它改变后,会知道通知UI更新。一个方法“Copy”,用来改变属性“WPF”的值,它通过命令的方式相应UI事件。
using System.ComponentModel;
using System.Windows.Input;
namespace WpfFirst
{
class Model : NotificationObject
{
private string _wpf = "WPF";
public string WPF
{
get { return _wpf; }
set
{
_wpf = value;
this.RaisePropertyChanged("WPF");
}
}
public void Copy(object obj)
{
this.WPF += " WPF";
}
}
}
4:定义ViewModel类
定义了一个命令属性"CopyCmd",聚合了一个Model对象"model"。这里的关键是,给CopyCmd命令指定响应命令的方法是model对象的“Copy”方法。
复制代码
using System;
namespace WpfFirst
{
class ViewModel
{
public DelegateCommand CopyCmd { get; set; }
public Model model { get; set; }
public ViewModel()
{
this.model = new Model();
this.CopyCmd = new DelegateCommand();
this.CopyCmd.ExecuteCommand = new Action<object>(this.model.Copy);
}
}
}
5:定义View
MainWindow.xaml代码:我们能看到,TextBlock控件的text属性,绑定在model对象的WPF属性上; Button的click事件通过命令绑定到CopyCmd命令属性。
复制代码
<Windowx:Class="WpfFirst.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350"Width="525">
<Grid>
<StackPanel VerticalAlignment="Center" >
<TextBlock Text="{Binding model.WPF}"Height="208" TextWrapping="WrapWithOverflow"></TextBlock>
<Button Command="{Binding CopyCmd}" Height="93"Width="232">Copy</Button>
</StackPanel>
</Grid>
</Window>
MainWindow.xaml.cs代码:它的工作知识把ViewModel对象赋值到DataContext属性,指定View的数据源就是这个ViewModel。
using System.Windows;
namespace WpfFirst
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new ViewModel();
}
}
}
6.运行结果。每当我们点击按钮,界面就是被更新了,因为Copy方法改变了WFP属性的值。
写这个简单的例子,就是为了直观地了解MVVM的编程模式。在实际开发中,不管程序有多复杂,也就是增加Model, View, ViewModel,和其他的一些辅助类(Helpers or Services)了,模式不会改变。
3:WPF与WINFORM区别
WPF开发于WinForm之后,从技术发展的角度,WPF比WinForm先进是不容置疑的。我觉得WPF相比于WinForm有下面的一些较好的特性:
1:解决Window Handle问题
在Windows GDI或WinForm开发中复杂的GUI应用程序,会使用的大量的控件,如Grid等。而每个控件或Grid cell都是一个小窗口,会使用一个Window handle,尽管控件厂商提供了很多优化办法,但还是会碰到Out of Memory或"Error Create Window handle",而导致程序退出。
WPF彻底改变了控件显示的模式,控件不在使用窗口,也就不会占用Window handle。理论上,如果一个WPF只有一个主窗口的话,WPF只会使用一个Window handle(如果忽略用于Dispatcher的隐藏窗口的话)。所以WPF GUI程序不会出现Window handle不够用的情况。
2: 多线程的处理
在WinForm程序开发时,最头疼的一个问题就是,worker线程修改控件的属性而导致程序崩溃,而且这种非法操作并不是每次都失败。WinForm控件提供了InvokeRequired属性来判断当前线程是不是控件创建线程。问题是当控件树很深是,这个属性会比较慢。
WPF开始设计的时候,就考虑到了多线程的问题。大部分的WPF类都继承于DispatcherObject。DispatcherObject实际就是对Dispatcher的一个简单封装。Dispatcher提供了类似InvokeRequired的方法(CheckAccess)。这个方法只是比较线程的ID,所以会很快。另外,Dispatcher提供了优先队列,异步调用,Timer等功能,简化了开发多线程GUI程序。
控件的Composition
在WinForm如果要实现一个有Checkbox的下拉菜单,将不得不处理复杂的Window消息。而通过WPF控件的Content Model和Layout系统,WPF控件可以包括任何类型的控件,甚至.Net CLR对象。很多现代的控件厂商也提供了Composition的控件,实现方法和WPF的Content模型也比较相似。WPF开发团队应该借鉴了Infragistics的很多想法。有了这个基础,开发新的WPF控件更加简单了。
3: XAML
个人觉得XAML应该是WPF中比较划时代的东东。通过XAML,我们可以用文本的方式描述复杂的Object Graph。这个想法在VB中就有了,不过XAML更简化,以便于使用工具来生成XAML。通过Command,Routing Event等机制,界面设计人员和程序员有比较清楚的界限。
4: DependencyProperty
在WinForm开发中,经常碰到的问题就是一个控件的值变了,其他控件也会跟着改变。解决办法,要不是通过写代码,要不是通过数据绑定,前者是界面和代码没法分开,后者还不够灵活。而WPF在这方面通过XAML可以简单的把相关的属性联系起来,通过Extension可以实现复杂的绑定关系。
总的来说,我觉得WPF应该是GUI发展的一个延续,原来GUI中复杂的东西,现在通过简单的文本就可以实现
第八章 EntityFramework
1:什么是EntityFramework
ADO.NET Entity Framework是微软以 ADO.NET为基础所发展出来的对象关系对应 (O/R Mapping)解决方案,早期被称为 ObjectSpace,现已经包含在 Visual Studio 2008 Service Pack 1以及 .NET Framework 3.5 Service Pack 1中发表。
ADO.NET Entity Framework 以 Entity Data Model (EDM)为主,将数据逻辑层切分为三块,分别为 Conceptual Schema, Mapping Schema与 Storage Schema三层,其上还有 Entity Client,Object Context以及 LINQ可以使用。
长久以来,程序设计师和数据库总是保持着一种微妙的关系,在商用应用程序中,数据库一定是不可或缺的元件,这让程序设计师一定要为了连接与访问数据库而去
学习 SQL 指令,因此在信息业中有很多人都在研究如何将程序设计模型和数据库集成在一起,对象关系对应 (Object-Relational Mapping) 的技术就是由此而生,像Hibernate或NHibernate都是这个技术下的产物,而微软虽然有了ADO.NET这个数据访问的利器,但却没有像NHibernate这样的对象对应工具,因此微软在.NET Framework 2.0发展时期,就提出了一个ObjectSpace的概念,ObjectSpace可以让应用程序可以用完全对象化的方法连接与访问数据库,其技术概念与NHibernate相当类似,然而ObjectSpace工程相当大,在.NETFramework 2.0完成时仍无法全部完成,因此微软将ObjectSpace纳入下一版本的.NETFramework中,并且再加上一个设计的工具(Designer),构成了现在的ADO.NET Entity Framework。
Entity Framework 利用了抽象化数据结构的方式,将每个数据库对象都转换成应用程序对象 (entity),而数据字段都转换为属性 (property),关系则转换为结合属性 (association),让数据库的 E/R 模型完全的转成对象模型,如此让程序设计师能用最熟悉的编程语言来调用访问。而在抽象化的结构之下,则是高度集成与对应结构的概念层、对应层和储存层,以及支持 Entity Framework 的数据提供者 (provider),让数据访问的工作得以顺利与完整的进行。
(1) 概念层:负责向上的对象与属性显露与访问。
(2) 对应层:将上方的概念层和底下的储存层的数据结构对应在一起。
(3) 储存层:依不同数据库与数据结构,而显露出实体的数据结构体,和Provider一起,负责实际对数据库的访问和 SQL的产生。
2:架构
概念层结构
概念层结构定义了对象模型 (Object Model),让上层的应用程序码可以如面向对象的方式般访问数据,概念层结构是由 CSDL (Conceptual Schema Definition Language) 所撰写。
一份概念层结构定义如下所示:
<?xml version="1.0" encoding="utf-8"?>
<Schema Namespace="Employees"Alias="Self"xmlns="http://schemas.microsoft.com/ado/2006/04/edm">
<EntityContainer Name="EmployeesContext">
<EntitySet Name="Employees"EntityType="Employees.Employees" />
</EntityContainer>
<EntityType Name="Employees">
<Key>
<PropertyRef Name="EmployeeId" />
</Key>
<Property Name="EmployeeId" Type="Guid"Nullable="false" />
<Property Name="LastName" Type="String"Nullable="false" />
<Property Name="FirstName" Type="String"Nullable="false" />
<Property Name="Email" Type="String"Nullable="false" />
</EntityType>
</Schema>
对应层结构
对应层结构负责将上层的概念层结构以及下层的储存体结构中的成员结合在一起,以确认数据的来源与流向。对应层结构是由 MSL (Mapping Specification Language)所撰写2。
一份对应层结构定义如下所示:
<?xml version="1.0" encoding="utf-8"?>
<Mapping Space="C-S" xmlns="urn:schemas-microsoft-com:windows:storage:mapping:CS">
<EntityContainerMappingStorageEntityContainer="dbo"CdmEntityContainer="EmployeesContext">
<EntitySetMapping Name="Employees"StoreEntitySet="Employees"TypeName="Employees.Employees">
<ScalarProperty Name="EmployeeId"ColumnName="EmployeeId" />
<ScalarProperty Name="LastName"ColumnName="LastName" />
<ScalarProperty Name="FirstName"ColumnName="FirstName" />
<ScalarProperty Name="Email"ColumnName="Email" />
</EntitySetMapping>
</EntityContainerMapping>
</Mapping>
储存层结构
储存层结构是负责与数据库管理系统 (DBMS)中的数据表做实体对应 (Physical Mapping),让数据可以输入正确的数据来源中,或者由正确的数据来源取出。它是由SSDL (Storage Schema Definition Language)所撰写3。
一份储存层结构定义如下所示:
?xml version="1.0" encoding="utf-8"?>
<Schema Namespace="Employees.Store"Alias="Self"
Provider="System.Data.SqlClient"
ProviderManifestToken="2005"
xmlns="http://schemas.microsoft.com/ado/2006/04/edm/ssdl">
<EntityContainer Name="dbo">
<EntitySet Name="Employees" EntityType="Employees.Store.Employees"/>
</EntityContainer>
<EntityType Name="Employees">
<Key>
<PropertyRef Name="EmployeeId" />
</Key>
<Property Name="EmployeeId"Type="uniqueidentifier" Nullable="false" />
<Property Name="LastName" Type="nvarchar"Nullable="false" MaxLength="50" />
<Property Name="FirstName"Type="nvarchar" Nullable="false" />
<Property Name="Email" Type="nvarchar"Nullable="false" />
</EntityType>
</Schema>
3:用户端
当定义好 Entity Data Model的 CS/MS/SS 之后,即可以利用 ADO.NET Entity Framework的用户端来访问 EDM,EDM中的数据提供者会向数据来源访问数据,再传回用户端。
目前 ADO.NET Entity Framework有三种用户端4:
Entity Client
Entity Client 是 ADO.NET Entity Framework中的本地用户端(Native Client),它的对象模型和 ADO.NET的其他用户端非常相似,一样有 Connection, Command, DataReader等对象,但最大的差异就是,它有自己的SQL指令 (Entity SQL),可以用 SQL的方式访问EDM,简单的说,就是把 EDM当成一个实体数据库。
// Initialize the EntityConnectionStringBuilder.
EntityConnectionStringBuilder entityBuilder = newEntityConnectionStringBuilder();
//Set the provider name.
entityBuilder.Provider = providerName;
// Set the provider-specific connection string.
entityBuilder.ProviderConnectionString = providerString;
// Set the Metadata location.
entityBuilder.Metadata =@"res://*/AdventureWorksModel.csdl|
res://*/AdventureWorksModel.ssdl|
res://*/AdventureWorksModel.msl";
Console.WriteLine(entityBuilder.ToString());
using (EntityConnection conn = newEntityConnection(entityBuilder.ToString()))
{
conn.Open();
Console.WriteLine("Just testing the connection.");
conn.Close();
}
Object Context
由于 Entity Client太过于制式,而且也不太符合 ORM的精神,因此微软在 Entity Client的上层加上了一个供编程语言直接访问的界面,它可以把EDM当成对象般的访问,此界面即为 Object Context (Object Service)。
在 Object Context中对 EDM 的任何动作,都会被自动转换成 Entity SQL送到 EDM 中执行。
// Get the contacts with the specified name.
ObjectQuery<Contact> contactQuery = context.Contact
.Where("it.LastName = @ln AND it.FirstName = @fn",
new ObjectParameter("ln", lastName),
new ObjectParameter("fn", firstName));
LINQ to Entities
Object Context 将 EDM的访问改变为一种对对象集合的访问方式,这也就让 LINQ有了发挥的空间,因此 LINQ to Entities也就由此而生,简单的说,就是利用 LINQ来访问 EDM,让LINQ的功能可以在数据库中发挥。
using (AdventureWorksEntities AWEntities = newAdventureWorksEntities())
{
ObjectQuery<Product> products = AWEntities.Product;
IQueryable<Product> productNames =
from p in products
select p;
4:开发工具
目前 ADO.NET Entity Framework的开发,在 Visual Studio 2008中有充份的支持,在安装 Visual Studio 2008 Service Pack 1后,文件范本中即会出现 ADO.NET实体数据模型 (ADO.NET Entity Data Model)可让开发人员利用 Entity Model Designer来设计 EDM,EDM亦可由记事本或文本编辑器所编辑。
5:派生服务
主条目:ADO.NET Data Services
微软特别针对了网络上各种不同的应用程序 (例如 AJAX, Silverlight, Mashup 应用程序)开发了一个基于 ADO.NET Entity Framework 之上的服务,称为 ADO.NET Data Services (项目代号为Astoria),并与 ADO.NET Entity Framework 一起包装在.NET Framework 3.5 Service Pack 1中发表。
继续完善中。。。。。。
第一章:memcached基础
1.1 memcached介绍
memcached 是以LiveJournal 旗下Danga Interactive 公司的Brad Fitzpatric 为首开发的一款软件。现在已成为 mixi、 hatena、 Facebook、 Vox、LiveJournal等众多服务中提高Web应用扩展性的重要因素。
许多Web应用都将数据保存到RDBMS中,应用服务器从中读取数据并在浏览器中显示。但随着数据量的增大、访问的集中,就会出现RDBMS的负担加重、数据库响应恶化、网站显示延迟等重大影响。
这时就该memcached大显身手了。memcached是高性能的分布式内存缓存服务器。一般的使用目的是,通过缓存数据库查询结果,减少数据库访问次数,以提高动态Web应用的速度、提高可扩展性。
1.2 memcached的特征
memcached作为高速运行的分布式缓存服务器,具有以下的特点。
· 协议简单
· 基于libevent的事件处理
· 内置内存存储方式
· memcached不互相通信的分布式
1.2.1 协议简单
memcached的服务器客户端通信并不使用复杂的XML等格式,而使用简单的基于文本行的协议。因此,通过telnet 也能在memcached上保存数据、取得数据。
1.2.2 基于libevent的事件处理
libevent是个程序库,它将Linux的epoll、BSD类操作系统的kqueue等事件处理功能封装成统一的接口。即使对服务器的连接数增加,也能发挥O(1)的性能。 memcached使用这个libevent库,因此能在Linux、BSD、Solaris等操作系统上发挥其高性能。关于事件处理这里就不再详细介绍,可以参考Dan Kegel的The C10K Problem。
1.2.3 基于libevent的事件处理
为了提高性能,memcached中保存的数据都存储在memcached内置的内存存储空间中。由于数据仅存在于内存中,因此重启memcached、重启操作系统会导致全部数据消失。另外,内容容量达到指定值之后,就基于LRU(Least Recently Used)算法自动删除不使用的缓存。 memcached本身是为缓存而设计的服务器,因此并没有过多考虑数据的永久性问题。
1.2.4 memcached不互相通信的分布式
memcached尽管是“分布式”缓存服务器,但服务器端并没有分布式功能。各个memcached不会互相通信以共享信息。那么,怎样进行分布式呢?这完全取决于客户端的实现。本连载也将介绍memcached的分布式。
1.3 安装memcached
memcached的安装比较简单,这里稍加说明。
memcached支持许多平台。
· Linux
· FreeBSD
· Solaris (memcached 1.2.5以上版本)
· Mac OS X
· Windows
下载memcached:http://www.danga.com/memcached/download.bml
memcached,监听TCP端口11211 最大内存使用量为64M。调试信息的内容大部分是关于存储的信息,下次连载时具体说明。
这里使用的memcached启动选项的内容如下。
选项 |
说明 |
-p |
使用的TCP端口。默认为11211 |
-m |
最大内存大小。默认为64M |
-vv |
用very vrebose模式启动,调试信息和错误输出到控制台 |
-d |
作为daemon在后台启动 |
1.4 客户端连接
许多语言都实现了连接memcached的客户端,其中以Perl、PHP为主。仅仅memcached网站上列出的语言就有
· Perl
· PHP
· Python
· Ruby
· C#
· C/C++
· Lua
等等。
· memcached客户端API:http://www.danga.com/memcached/apis.bml
这里介绍通过mixi正在使用的Perl库链接memcached的方法。使用Cache::Memcached
Perl的memcached客户端有
· Cache::Memcached
· Cache::Memcached::Fast
· Cache::Memcached::libmemcached
等几个CPAN模块。这里介绍的Cache::Memcached是memcached的作者Brad Fitzpatric的作品,应该算是memcached的客户端中应用最为广泛的模块了。
选项 |
说明 |
servers |
用数组指定memcached服务器和端口 |
compress_threshold |
数据压缩时使用的值 |
namespace |
指定添加到键的前缀 |
另外,Cache::Memcached通过Storable模块可以将Perl的复杂数据序列化之后再保存,因此散列、数组、对象等都可以直接保存到memcached中。
1.5 操作数据
1.5.1 保存数据
向memcached保存数据的方法有
· add
· replace
· set
它们的使用方法都相同:
.add( '键', '值', '期限' );
.replace( '键', '值', '期限' );
.set( '键', '值', '期限' );
向memcached保存数据时可以指定期限(秒)。不指定期限时,memcached按照LRU算法保存数据。这三个方法的区别如下:
选项 |
说明 |
add |
仅当存储空间中不存在键相同的数据时才保存 |
replace |
仅当存储空间中存在键相同的数据时才保存 |
set |
与add和replace不同,无论何时都保存 |
1.5.2 获取数据
获取数据可以使用get和get_multi方法。
.get('键');
.get_multi('键1', '键2', '键3', '键4', '键5');
一次取得多条数据时使用get_multi。get_multi可以非同步地同时取得多个键值,其速度要比循环调用get快数十倍。
1.5.3 删除数据
删除数据使用delete方法,不过它有个独特的功能。
$memcached->delete('键', '阻塞时间(秒)');
删除第一个参数指定的键的数据。第二个参数指定一个时间值,可以禁止使用同样的键保存新数据。此功能可以用于防止缓存数据的不完整。但是要注意,set函数忽视该阻塞,照常保存数据
增一和减一操作
可以将memcached上特定的键值作为计数器使用。
.incr('键'); .add('键', 0) unless defined $ret;
增一和减一是原子操作,但未设置初始值时,不会自动赋成0。因此,应当进行错误检查,必要时加入初始化操作。而且,服务器端也不会对超过2<sup>32</sup>时的行为进行检查。
第二章: memcached内存存储
最近的memcached默认情况下采用了名为Slab Allocator的机制分配、管理内存。在该机制出现以前,内存的分配是通过对所有记录简单地进行malloc和free来进行的。但是,这种方式会导致内存碎片,加重操作系统内存管理器的负担,最坏的情况下,会导致操作系统比memcached进程本身还慢。Slab Allocator就是为解决该问题而诞生的。
2.1 Slab Allocator的原理
Slab Allocator的基本原理是按照预先规定的大小,将分配的内存分割成特定长度的块,以完全解决内存碎片问题。
Slab Allocation的原理相当简单。将分配的内存分割成各种尺寸的块(chunk),并把尺寸相同的块分成组(chunk的集合)(图1)。
而且,slab allocator还有重复使用已分配的内存的目的。也就是说,分配到的内存不会释放,而是重复利用。
2.1.1 Slab Allocation的主要术语
Page
分配给Slab的内存空间,默认是1MB。分配给Slab之后根据slab的大小切分成chunk。
Chunk
用于缓存记录的内存空间。
Slab Class
特定大小的chunk的组。
2.1.2 在Slab中缓存记录的原理
下面说明memcached如何针对客户端发送的数据选择slab并缓存到chunk中。
memcached根据收到的数据的大小,选择最适合数据大小的slab(图2)。 memcached中保存着slab内空闲chunk的列表,根据该列表选择chunk,然后将数据缓存于其中。
图2 选择存储记录的组的方法
实际上,Slab Allocator也是有利也有弊。下面介绍一下它的缺点。
2.1.3 Slab Allocator的缺点
Slab Allocator解决了当初的内存碎片问题,但新的机制也给memcached带来了新的问题。
这个问题就是,由于分配的是特定长度的内存,因此无法有效利用分配的内存。例如,将100字节的数据缓存到128字节的chunk中,剩余的28字节就浪费了(图3)。
图3 chunk空间的使用
对于该问题目前还没有完美的解决方案,但在文档中记载了比较有效的解决方案。
The most efficient wayto reduce the waste is to use a list of size classes that closely matches (ifthat's at all possible) common sizes of objects that the clients of thisparticular installation of memcached are likely to store.
就是说,如果预先知道客户端发送的数据的公用大小,或者仅缓存大小相同的数据的情况下,只要使用适合数据大小的组的列表,就可以减少浪费。
但是很遗憾,现在还不能进行任何调优,只能期待以后的版本了。但是,我们可以调节slab class的大小的差别。接下来说明growth factor选项。
2.1.4 使用GrowthFactor进行调优
memcached在启动时指定 Growth Factor因子(通过-f选项),就可以在某种程度上控制slab之间的差异。默认值为1.25。但是,在该选项出现之前,这个因子曾经固定为2,称为“powers of 2”策略。
让我们用以前的设置,以verbose模式启动memcached试试看:
$ memcached -f 2 -vv
下面是启动后的verbose输出:
slab class 1: chunk size 128 perslab 8192
slab class 2: chunk size 256 perslab 4096
slab class 3: chunk size 512 perslab 2048
slab class 4: chunk size 1024 perslab 1024
slab class 5: chunk size 2048 perslab 512
slab class 6: chunk size 4096 perslab 256
slab class 7: chunk size 8192 perslab 128
slab class 8: chunk size 16384 perslab 64
slab class 9: chunk size 32768 perslab 32
slab class 10: chunk size 65536 perslab 16
slab class 11: chunk size 131072perslab 8
slab class 12: chunk size 262144perslab 4
slab class 13: chunk size 524288perslab 2
可见,从128字节的组开始,组的大小依次增大为原来的2倍。这样设置的问题是,slab之间的差别比较大,有些情况下就相当浪费内存。因此,为尽量减少内存浪费,两年前追加了growth factor这个选项。
来看看现在的默认设置(f=1.25)时的输出(篇幅所限,这里只写到第10组):
slab class 1: chunk size 88 perslab 11915
slab class 2: chunk size 112 perslab 9362
slab class 3: chunk size 144 perslab 7281
slab class 4: chunk size 184 perslab 5698
slab class 5: chunk size 232 perslab 4519
slab class 6: chunk size 296 perslab 3542
slab class 7: chunk size 376 perslab 2788
slab class 8: chunk size 472 perslab 2221
slab class 9: chunk size 592 perslab 1771
slab class 10: chunk size 744 perslab 1409
可见,组间差距比因子为2时小得多,更适合缓存几百字节的记录。从上面的输出结果来看,可能会觉得有些计算误差,这些误差是为了保持字节数的对齐而故意设置的。
将memcached引入产品,或是直接使用默认值进行部署时,最好是重新计算一下数据的预期平均长度,调整growth factor,以获得最恰当的设置。内存是珍贵的资源,浪费就太可惜了。
接下来介绍一下如何使用memcached的stats命令查看slabs的利用率等各种各样的信息。
第三章:memcached删除机制
3.1 memcached删除数据原理
memcached不会释放已分配的内存。记录超时后,客户端就无法再看见该记录(invisible,透明),其存储空间即可重复使用。memcached内部不会监视记录是否过期,而是在get时查看记录的时间戳,检查记录是否过期。这种技术被称为lazy(惰性)expiration。因此,memcached不会在过期监视上耗费CPU时间。
3.1.1 缓存删除有效数据
memcached会优先使用已超时的记录的空间,但即使如此,也会发生追加新记录时空间不足的情况,此时就要使用名为 Least Recently Used(LRU)机制来分配空间。顾名思义,这是删除“最近最少使用”的记录的机制。因此,当memcached的内存空间不足时(无法从slab class 获取到新的空间时),就从最近未被使用的记录中搜索,并将其空间分配给新的记录。从缓存的实用角度来看,该模型十分理想。
不过,有些情况下LRU机制反倒会造成麻烦。memcached启动时通过“-M”参数可以禁止LRU,如下所示:
$ memcached -M -m 1024
启动时必须注意的是,小写的“-m”选项是用来指定最大内存大小的。不指定具体数值则使用默认值64MB。
指定“-M”参数启动后,内存用尽时memcached会返回错误。话说回来,memcached毕竟不是存储器,而是缓存,所以推荐使用LRU。
第四章:memcached分布算法
4.1 memcached的分布式
正如第1次中介绍的那样, memcached虽然称为“分布式”缓存服务器,但服务器端并没有“分布式”功能。服务器端仅包括 第2次、 第3次 前坂介绍的内存存储功能,其实现非常简单。至于memcached的分布式,则是完全由客户端程序库实现的。这种分布式是memcached的最大特点。
4.1.1 memcached的分布式是什么意思?
这里多次使用了“分布式”这个词,但并未做详细解释。现在开始简单地介绍一下其原理,各个客户端的实现基本相同。
下面假设memcached服务器有node1~node3三台,应用程序要保存键名为“tokyo”“kanagawa”“chiba”“saitama”“gunma” 的数据。
图1 分布式简介:准备
首先向memcached中添加“tokyo”。将“tokyo”传给客户端程序库后,客户端实现的算法就会根据“键”来决定保存数据的memcached服务器。服务器选定后,即命令它保存“tokyo”及其值。
图2 分布式简介:添加时
同样,“kanagawa”“chiba”“saitama”“gunma”都是先选择服务器再保存。
接下来获取保存的数据。获取时也要将要获取的键“tokyo”传递给函数库。函数库通过与数据保存时相同的算法,根据“键”选择服务器。使用的算法相同,就能选中与保存时相同的服务器,然后发送get命令。只要数据没有因为某些原因被删除,就能获得保存的值。
图3 分布式简介:获取时
这样,将不同的键保存到不同的服务器上,就实现了memcached的分布式。 memcached服务器增多后,键就会分散,即使一台memcached服务器发生故障无法连接,也不会影响其他的缓存,系统依然能继续运行。
接下来介绍第1次 中提到的Perl客户端函数库Cache::Memcached实现的分布式方法。
4.1.2 Memcached的分布式方法
Perl的memcached客户端函数库Cache::Memcached是 memcached的作者Brad Fitzpatrick的作品,可以说是原装的函数库了。
Cache::Memcached -search.cpan.org
该函数库实现了分布式功能,是memcached标准的分布式方法。
4.1.2.1根据余数计算分散
Cache::Memcached的分布式方法简单来说,就是“根据服务器台数的余数进行分散”。求得键的整数哈希值,再除以服务器台数,根据其余数来选择服务器。
下面将Cache::Memcached简化成以下的Perl脚本来进行说明。
use strict; use warnings; use String::CRC32; my @nodes = ('node1','node2','node3'); my @keys = ('tokyo', 'kanagawa', 'chiba', 'saitama', 'gunma'); foreach my $key (@keys) { my $crc = crc32($key); # CRC値 my $mod = $crc % ( $#nodes + 1 ); my $server = $nodes[ $mod ]; # 根据余数选择服务器 printf "%s => %s\n", $key, $server; }
Cache::Memcached在求哈希值时使用了CRC。
· String::CRC32 -search.cpan.org
首先求得字符串的CRC值,根据该值除以服务器节点数目得到的余数决定服务器。上面的代码执行后输入以下结果:
tokyo => node2
kanagawa => node3
chiba => node2
saitama => node1
gunma => node1
根据该结果,“tokyo”分散到node2,“kanagawa”分散到node3等。多说一句,当选择的服务器无法连接时,Cache::Memcached会将连接次数添加到键之后,再次计算哈希值并尝试连接。这个动作称为rehash。不希望rehash时可以在生成Cache::Memcached对象时指定“rehash => 0”选项。
4.1.2.2 根据余数计算分散的缺点
余数计算的方法简单,数据的分散性也相当优秀,但也有其缺点。那就是当添加或移除服务器时,缓存重组的代价相当巨大。添加服务器后,余数就会产生巨变,这样就无法获取与保存时相同的服务器,从而影响缓存的命中率。用Perl写段代码来验证其代价。
use strict;
use warnings;
use String::CRC32;
my @nodes = @ARGV;
my @keys = ('a'..'z');
my %nodes;
foreach my $key ( @keys ) {
my $hash = crc32($key);
my $mod = $hash % ( $#nodes + 1 );
my $server = $nodes[ $mod ];
push @{ $nodes{ $server } }, $key;
}
foreach my $node ( sort keys %nodes ) {
printf "%s: %s\n", $node, join ",", @{ $nodes{$node} };
}
这段Perl脚本演示了将“a”到“z”的键保存到memcached并访问的情况。将其保存为mod.pl并执行。
首先,当服务器只有三台时:
$ mod.pl node1 node2 nod3
node1: a,c,d,e,h,j,n,u,w,x
node2: g,i,k,l,p,r,s,y
node3: b,f,m,o,q,t,v,z
结果如上,node1保存a、c、d、e……,node2保存g、i、k……,每台服务器都保存了8个到10个数据。
接下来增加一台memcached服务器。
$ mod.pl node1 node2 node3 node4
node1: d,f,m,o,t,v
node2: b,i,k,p,r,y
node3: e,g,l,n,u,w
node4: a,c,h,j,q,s,x,z
添加了node4。可见,只有d、i、k、p、r、y命中了。像这样,添加节点后键分散到的服务器会发生巨大变化。26个键中只有六个在访问原来的服务器,其他的全都移到了其他服务器。命中率降低到23%。在Web应用程序中使用memcached时,在添加memcached服务器的瞬间缓存效率会大幅度下降,负载会集中到数据库服务器上,有可能会发生无法提供正常服务的情况。
mixi的Web应用程序运用中也有这个问题,导致无法添加memcached服务器。但由于使用了新的分布式方法,现在可以轻而易举地添加memcached服务器了。这种分布式方法称为 Consistent Hashing。
4.1.3 Consistent Hashing
Consistent Hashing如下所示:首先求出memcached服务器(节点)的哈希值,并将其配置到0~232的圆(continuum)上。然后用同样的方法求出存储数据的键的哈希值,并映射到圆上。然后从数据映射到的位置开始顺时针查找,将数据保存到找到的第一个服务器上。如果超过232仍然找不到服务器,就会保存到第一台memcached服务器上。
图4 Consistent Hashing:基本原理
从上图的状态中添加一台memcached服务器。余数分布式算法由于保存键的服务器会发生巨大变化而影响缓存的命中率,但Consistent Hashing中,只有在continuum上增加服务器的地点逆时针方向的第一台服务器上的键会受到影响。
图5 Consistent Hashing:添加服务器
因此,Consistent Hashing最大限度地抑制了键的重新分布。而且,有的Consistent Hashing的实现方法还采用了虚拟节点的思想。使用一般的hash函数的话,服务器的映射地点的分布非常不均匀。因此,使用虚拟节点的思想,为每个物理节点(服务器)在continuum上分配100~200个点。这样就能抑制分布不均匀,最大限度地减小服务器增减时的缓存重新分布。
通过下文中介绍的使用Consistent Hashing算法的memcached客户端函数库进行测试的结果是,由服务器台数(n)和增加的服务器台数(m)计算增加服务器后的命中率计算公式如下:
(1 - n/(n+m)) * 100
支持Consistent Hashing的函数库
本连载中多次介绍的Cache::Memcached虽然不支持Consistent Hashing,但已有几个客户端函数库支持了这种新的分布式算法。第一个支持Consistent Hashing和虚拟节点的memcached客户端函数库是名为libketama的PHP库,由last.fm开发。
· libketama- a consistent hashing algo for memcache clients – RJ ブログ- Users at Last.fm
至于Perl客户端,连载的第1次 中介绍过的Cache::Memcached::Fast和Cache::Memcached::libmemcached支持 Consistent Hashing。
· Cache::Memcached::Fast- search.cpan.org
· Cache::Memcached::libmemcached- search.cpan.org
两者的接口都与Cache::Memcached几乎相同,如果正在使用Cache::Memcached,那么就可以方便地替换过来。Cache::Memcached::Fast重新实现了libketama,使用ConsistentHashing创建对象时可以指定ketama_points选项。
my $memcached = Cache::Memcached::Fast->new({
servers => ["192.168.0.1:11211","192.168.0.2:11211"],
ketama_points => 150
});
另外,Cache::Memcached::libmemcached 是一个使用了Brain Aker开发的C函数库libmemcached的Perl模块。 libmemcached本身支持几种分布式算法,也支持Consistent Hashing,其Perl绑定也支持Consistent Hashing。
第五章 分布式缓存系统Memcached简介与实践
Memcached是什么?
Memcached是由DangaInteractive开发的,高性能的,分布式的内存对象缓存系统,用于在动态应用中减少数据库负载,提升访问速度。
Memcached能缓存什么?
通过在内存里维护一个统一的巨大的hash表,Memcached能够用来存储各种格式的数据,包括图像、视频、文件以及数据库检索的结果等。
Memcached快么?
非常快。Memcached使用了libevent(如果可以的话,在linux下使用epoll)来均衡任何数量的打开链接,使用非阻塞的网络I/O,对内部对象实现引用计数(因此,针对多样的客户端,对象可以处在多样的状态),使用自己的页块分配器和哈希表,因此虚拟内存不会产生碎片并且虚拟内存分配的时间复杂度可以保证为O(1).。
Danga Interactive为提升DangaInteractive的速度研发了Memcached。目前,LiveJournal.com每天已经在向一百万用户提供多达两千万次的页面访问。而这些,是由一个由web服务器和数据库服务器组成的集群完成的。Memcached几乎完全放弃了任何数据都从数据库读取的方式,同时,它还缩短了用户查看页面的速度、更好的资源分配方式,以及Memcache失效时对数据库的访问速度。
Memcached的特点
Memcached的缓存是一种分布式的,可以让不同主机上的多个用户同时访问,因此解决了共享内存只能单机应用的局限,更不会出现使用数据库做类似事情的时候,磁盘开销和阻塞的发生。
Memcached的使用
一 Memcached服务器端的安装(此处将其作为系统服务安装)
下载文件:memcached 1.2.1for Win32 binaries (Dec 23, 2006)
1 解压缩文件到c:\memcached
2 命令行输入'c:\memcached\memcached.exe -d install'
3 命令行输入'c:\memcached\memcached.exe -d start' ,该命令启动 Memcached ,默认监听端口为 11211
通过 memcached.exe -h 可以查看其帮助
二 .NET memcached clientlibrary
下载文件:https://sourceforge.net/projects/memcacheddotnet/
里面有.net1.1 和 .net2.0的两种版本 还有一个不错的例子。
三 应用
1 将Commons.dll,ICSharpCode.SharpZipLib.dll,log4net.dll,Memcached.ClientLibrary.dll等放到bin目录
2 引用Memcached.ClientLibrary.dll
3 代码
1 namespace Memcached.MemcachedBench
2 {
3 using System;
4 using System.Collections;
5
6 using Memcached.ClientLibrary;
7
8 public class MemcachedBench
9 {
10 [STAThread]
11 public static void Main(String[] args)
12 {
13 string[] serverlist = { "10.0.0.131:11211", "10.0.0.132:11211" };
14
15 //初始化池
16 SockIOPool pool = SockIOPool.GetInstance();
17 pool.SetServers(serverlist);
18
19 pool.InitConnections = 3;
20 pool.MinConnections = 3;
21 pool.MaxConnections = 5;
22
23 pool.SocketConnectTimeout = 1000;
24 pool.SocketTimeout = 3000;
25
26 pool.MaintenanceSleep = 30;
27 pool.Failover = true;
28
29 pool.Nagle = false;
30 pool.Initialize();
31
32 // 获得客户端实例
33 MemcachedClient mc = new MemcachedClient();
34 mc.EnableCompression = false;
35
36 Console.WriteLine("------------测 试-----------");
37 mc.Set("test", "my value"); //存储数据到缓存服务器,这里将字符串"my value"缓存,key 是"test"
38
39 if (mc.KeyExists("test")) //测试缓存存在key为test的项目
40 {
41 Console.WriteLine("test is Exists");
42 Console.WriteLine(mc.Get("test").ToString()); //在缓存中获取key为test的项目
43 }
44 else
45 {
46 Console.WriteLine("test not Exists");
47 }
48
49 Console.ReadLine();
50
51 mc.Delete("test"); //移除缓存中key为test的项目
52
53 if (mc.KeyExists("test"))
54 {
55 Console.WriteLine("test is Exists");
56 Console.WriteLine(mc.Get("test").ToString());
57 }
58 else
59 {
60 Console.WriteLine("test not Exists");
61 }
62 Console.ReadLine();
63
64 SockIOPool.GetInstance().Shutdown(); //关闭池, 关闭sockets
65 }
66 }
67 }
4 运行结果