面向服务的计算和web数据管理第2章 多线程分布式计算_第1页
面向服务的计算和web数据管理第2章 多线程分布式计算_第2页
面向服务的计算和web数据管理第2章 多线程分布式计算_第3页
面向服务的计算和web数据管理第2章 多线程分布式计算_第4页
面向服务的计算和web数据管理第2章 多线程分布式计算_第5页
已阅读5页,还剩338页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

面向服务的计算和web数据管理第2章多线程分布式计算从本章开始,我们将不仅学习概念,还学习用编程语言实现这些概念。我们相信通过具体编码能够更好的解释这些概念以及它们的使用。我们假设读者熟悉面向对象的计算,并且用过一种面向对象编程语言如:C++、Java或者C#编写过面向对象的程序。但是,为了使那些没有用过C#的读者也能理解本章的内容,我们在本章的开始对C#和.NET作简短的介绍。然后,转移到多线程和多任务的一般性问题上,包括并行处理、同步、死锁和执行顺序,这些是分布式计算范型的基础。为了更好的理解所讨论的概念,我们将用Java和C#开发多线程程序。最后,我们讨论分布式计算异常和事件驱动编程方法。这些概念和技术被广泛用于分布式面向服务的计算。

本章和本书中其他章节的联系不是很紧密,因为本章提到的大多数问题和技术都在面向服务开发环境中遇到过,因此应用构建者可以摆脱这些问题。如果一个服务提供者使用服务软件,如IIS,大部分问题也都属于服务软件的问题。但是如果服务提供者想写自己的服务,那么学习本章的概念还是很重要的。如果读者关注使用先进的工具和基础设施进行面向服务的软件开发,那么可以跳过本章。

在本节,我们将简要地介绍C#。如果你熟悉C

#,可以跳过这一节。

C#和Java都是面向对象的编程语言,并且它们是用来写多线程程序以及创建SOC服务的两个主要编程语言。在Java中,每一个线程通过对象创建。在C#中,一个线程能通过对象或对象中的函数(方法)创建。在面向服务的应用中,服务的组成部分用Java或C#对象定义,用开放标准接口包装这些对象使其成为服务。2.1C#和.Net介绍

C#继承了C/C++语言的许多语法,并支持Java的许多特征。它是一种强类型语言并自动进行内存回收。.Net通用库中有一个很大的函数集支持C#,面向对象和面向服务的软件开发人员可复用这些函数。我们假定读者已经掌握了Java或者C++,所以本节只对C#做简单介绍。2.1.1C#与.Net入门

微软的VisualStudio.Net是一种支持多种编程语言和多种编程范型的编程环境。如图2.1所示,在第一步编译中,高级语言的程序编译成一种叫做中间语言(IL)的低级语言,从表面看,中间语言类似于汇编语言。中间语言程序通过公共语言运行时(CLR)环境进行管理和执行。它类似于Java的字节码,目的使CLR独立于高级程序语言。

图2.1微软VisualStudio.Net编程环境

与Java环境不同的是,.Net框架与语言无关。虽然C#被认为是它的标志性语言,但是.Net框架并不是为某种特定语言设计的。当开发者使用他们选择的高级语言编写应用程序的时候,可以使用公共框架类库(FCL)和环境的功能。公共框架类库是一个通用系统类型,这使得它可以支持不同的编程语言。基于类型系统,任何编程语言,例如X,都可以很容易地集成到系统中。唯一要做的工作是写一个将X转换成IL的编译器。中间语言程序由实时(JIT)编译器和CLR执行,JIT编译器采用边使用边编译的策略,当方法被第一次调用时,它动态地为内部数据结构分配内存块。换句话说,JIT编译器介于编译和解释执行之间。

现在让我们写一个输出字符串“Hello,World!”的C#程序。

在VisualStudio.Net环境下执行这个C#程序,遵循以下步骤:

(1)从Windows的“开始”菜单启动.Net。

(2)选择.Net中的菜单“文件”—“新建”—“项目...”,一个“新项目”对话框弹出,在这里你可以选择不同的编程语言,包括C/C++、J#(Java)以及C#。

(3)一旦你在框的左侧选择C#,就可以进一步选择一个模板帮助你开发应用,例如:

①选择“控制台应用”启动文本和基于命令行的编程模板;

②选择“Windows应用”启动一个基于表格的应用模板,该模板允许你定义图形用户界面。

(4)在同一对话框的底部,你可以选择项目名称、选择保存该项目的路径、选择方案名称,你可以在同一个方案中放入多个工程。

(5)点击“OK”。

这时一个拥有相应库(取决于你所选择的模板)的项目模板将被创建。如果选择的是C#,当你创建一个新项目时,就能够在扩展名为.cs的文件中输入C#程序。

下面的程序展示了一个更复杂的例子。这个程序管理举重比赛的得分/重量。要求用户输入四名运动员的姓名和举重重量,然后程序输出获胜者的姓名和举重重量。

这个程序展示了一个典型程序中的许多重要问题,包括:输入、输出、字符串到整数的转换;定义整型、字符串、整型数组的引用、字符串数组的引用、创建一个数组对象、循环、定义静态函数、参数传递和函数调用。程序中的注释提供了更为详细的解释。2.1.2C#和C++的比较

与C++相比,C#的特征更类似于Java。表2.1将C#和C++的主要特征作了对比。可以看出,C#向Java的自动管理方式靠拢,同时在尽可能的情况下保持C++的特征。

表2.2和表2.3分别列出了C#和C++所支持的数据类型。C/C++中一些数据类型依赖于机器,因此,这种语言标准只规定了最小范围。例如,整数类型int的长度是16、32还是64Byte,取决于实现该语言的计算机体系结构。

表2.1C++和C#(Java)特征比较

表2.2 C/C++中的数据类型

表2.3C#中的数据类型另一方面,C#运行在虚拟机.Net框架上,所有的数据类型是机器无关的。Int类型的长度总是32个字节。为了更明确,你可以使用Int32代替,特别是小数类型,它采用较少的位表示指数部分,而使用多个位表示浮点数的小数部分,使它成为高精度的数据类型。小数类型通常用于货币类的计算。

2.1.3名字空间和using指令

名字空间用于类的分组,“using<namespace>”用于引用名字空间的类,就像程序中使用库函数一样。下面的代码段显示了C#程序的基本组成。

using<namespace>//usingexistingnamespaceaslibrary

namespacemyNamespace1//definemyownnamespace

classmyclass1{

publicstaticvoidMain(){

}classmyclass2{

publicdoublePiVlue(){

}

}

C#中不存在C/C++中的头文件,而是用名字空间引用一组类和库来代替。代码的第一行显示了这种应用。程序员可以定义自己的名字空间,如上面代码的第二行所示,把多个类归为一组。一个程序员在同一程序中可定义多个名字空间以防止命名冲突。例如:

namespaceVirtualStore{

namespaceCustomer{

//definecustomerclasses

classShoppingCartOrder(){...}

}

namespaceAdmin{//defineadministrationclasses

classReportGenerator(){...}

}

}

“using”指令告诉编译器在哪里寻找程序中使用的名字空间中类的成员方法。这些成员方法的使用和库函数一样。例如:

usingVirtualStore;

此外,.Net框架的GUI功能、表格可以通过指令访问:

usingSystem.Windows.Forms;

...

privateButtonButton1;//classButtonisdefinedinSystem.Windows.Forms;

指令使用的另一种形式如下所示,在程序中对每一个引用作完全限定:

privateSystem.Windows.Forms.ButtonButton1;2.1.4C#中的队列例子

为了了解C#与C++的具体区别并编写C#程序,我们介绍一个C#编写的队列例子(Chen,2006)。从这个例子可以看到,当你学习过C++或者Java后,就能很轻松地学习C#。类、对象、构造函数、方法的概念将在后续章节中通过示例进一步介绍。

usingSystem;

namespaceQueueApplication{//classisdefinedwithincurlybracket

classQueue{privateInt32queuesize;

protectedInt32[]buffer;//declareareferenceonly

protectedInt32front;

protectedInt32rear;

publicQueue(){//constructor

front=0;rear=0;

queue_size=10;

buffer=newInt32[queue_size];//objectiscreatedhere

}publicQueue(Int32n){//constructor

front=0;rear=0;

queue_size=n;

buffer=newInt32[queue_size];

}

publicvoidenqueue(Int32v){

if(rear<queuesize)

buffer[rear++]=v;

else

if(compact())buffer[rear++]=v;

}

publicInt32dequeue(){

if(front<rear)

returnbuffer[front++];

else{

Console.WriteLine("Error:Queueempty");

return-1;

}

}privateboolcompact(){

if(front==0){

Console.WriteLine("Error:Queueoverflow");

returnfalse;

}

else{

for(Int32i=0;i<rear-front;i++)

buffer[i]=buffer[i+front];

rear=rear-front;

front=0;returntrue;

}

}

}

classPriQueue:Queue//inheritance{

publicPriQueue(Int32n):base(n){}

publicInt32getMax(){

inti,max,imax;

if(front<rear){

max=buffer[front];imax=front;//imaxholdstheindexofcurrentmax

for(i=front;i<rear;i++)

if(max<buffer[i]){max=buffer[i];imax=i;}

for(i=imax;i<rear-1;i++)

buffer[i]=buffer[i+1];//removethemaxvalue

rear=rear-1;

returnmax;

}

else{Console.WriteLine("Error:Queueempty");

return-1;}

}

}

classmyMainClass{

staticvoidMain(){

Console.WriteLine("Pleaseenterthequeuesize");

stringstr=Console.ReadLine();//readastring

Int32n=Convert.ToInt32(str);//convertstringtoint

QueueQ1=newQueue(n);

Q1.enqueue(12);Q1.enqueue(36);

Q1.enqueue(24);

Int32X=Q1.dequeue();

Int32Y=Q1.dequeue();

Int32Z=Q1.dequeue();

Console.WriteLine("X={0}Y={1}Z={2}",X,Y,Z);

PriQueueQ2=newPriQueue(n);

Q2.enqueue(12);

Q2.enqueue(36);

Q2.enqueue(24);X=Q2.getMax();

Y=Q2.dequeue();

Z=Q2.dequeue();

Console.WriteLine("X={0}Y={1}Z={2}",X,Y,Z);

}

}

}

程序执行后的结果如下:

Pleaseenterthequeuesize

5

X=12Y=36Z=24

X=36Y=12Z=24

与上一节举重的例子相比,这个例子进一步说明了C#的特性,包括:多个类的名字空间的定义、类的构造函数、类的继承以及对基类构造函数的扩展。2.1.5C#中的类和对象

和Java一样,所有的C#程序需要一个唯一的程序入口点,或Main方法,作为类中的成员方法来实现。这不同于C++中的Main函数不属于任何类。在C#程序中,Main函数的位置由编译器决定,与哪个类定义Main函数无关。Main函数必须定义为静态函数,但是否接收参数或返回值是可选的。一个可选的public访问权限修饰符通知C#编译器任何人都可以调用这个成员方法。static关键字意味着调用Main函数不需要对象实例。很明显,Main函数是第一个被执行的方法,并且没有其他的方法能调用Main函数。

类是一种用户自定义类型,一种结构化设计,一种具有该类型的对象功能的描述。一个类由若干成员组成。每一个成员要么是数据成员(也叫做变量),要么是成员函数(也叫做方法)。与类同名的成员函数叫做构造函数。构造函数初始化类中的数据成员。其他成员函数操纵数据成员,并为其他类提供可复用的功能。

类的实例叫做对象。做个比喻,一个类可以看成是制作饼干的器具,那么对象就是该器具制作出来的饼干。当为对象建立引用后,这个对象可通过引用访问。用new操作符实例化一个类,就是在堆中为实例或对象分配存储空间存储其成员信息。对于所有的面向对象语言,用new操作符创建的对象,都从堆中分配存储空间存储类中定义的所有成员,如变量、常量、方法。

在C#中,类的静态成员的访问和Java一样,用类名和“.”操作符。<className>.<memberName>

Console.WriteLine("HelloWorld!");

其中memberName是一个方法调用或者变量名。

同Java一样,用“.”操作符也能进行引用对象成员的访问。在C++中,如果引用是一个指针,那么使用指针运算符“->”。

<referenceName>.<memberName>

time.printStandard();

其中memberName是一个方法调用或者变量名。

C++提供了第二种方式来定义对象,即简单实例化,不需要使用new操作符。这种方法不像堆中的引用类型,而像栈中的局部类型。在C#中,“new”关键字是创建对象实例的唯一方法。

类的语法可以通过下面的语法图描述,方括号内是可选项。

[attributes][modifiers]class<className>[:baseClassName]

{[classbody]

}[;]

其中,attributes被看做是内联注释和声明语句,这些语句可以添加到类、成员、参数或者其他编码元素中。通过一个称为reflection的库,可以检索这些附加的信息并在运行时供其他代码使用。attributes提供了把信息和声明相关连的通用方法,在许多情况下,它是一个强大的工具。

modifiers表示三种访问控制符,分别是public,protected和private,它们与C++中的访问控制符的含义相同。如果没有明确定义访问权限,C#和C++都默认为private。

还有一些其他的方法修饰符包括sealed、override、virtual,以及处理类的继承和范围的类修饰符abstract。

在C++中,程序员可以选择在类声明中定义类成员或者使用作用域操作符在类声明外定义类成员。在C#中,所有类成员必须在类的大括号内定义。将同一个类中相关联的对象归为一组的简单思想可以设计出模块化更好的代码。

面向对象编程的主要特征就是将一个应用分解成多个类。其中的一个类包含Main()方法,而其他类包含可重用的类成员和方法。

让我们考虑一个帮助人们准备旅游的程序,其中包括美元同当地所需货币之间的换算,将当地温度转化为华氏温度。该程序被分解成四个类,如图2.2所示。

图2.2问题被分解成四个类下面给出了实现旅游准备工作的示例代码。myCost类中给出了带一个参数的构造函数。由于参数被多个成员函数使用,通过构造函数传递参数就会更有效率。当用new操作符实例化对象时,该参数值被传给对象。而其他两个类CurrencyConversion和TemperatureConversion则不需要构造函数。在这些类中,参数仅仅被一个成员函数使用,因此,我们可以直接将参数传递给成员函数,而不是建立数据成员保存参数值。usingSystem;

classTravelPreparation{//MainClass

staticvoidMain(string[]args){//Themainmethod

Console.WriteLine("Pleaseenterthenumberofdaysyouwilltravel");

Stringstr=Console.ReadLine();//readastringofcharacters

Int32daysToStay=Convert.ToInt32(str);//Convertstringtointeger

myCostusdObject=newmyCost(daysToStay);//Createanobject

intusdCash=usdObject.total();//Callamethodintheobject

Console.WriteLine("Pleaseenterthecountrynameyouwilltravelto");Stringcountry=Console.ReadLine();

CurrencyConversionexchange=newCurrencyConversion();

DoubleAmountLocal=exchange.usdToLocalCurrency(country,usdCash);

Console.WriteLine("Theamountoflocalcurrencyis:"+AmountLocal);

Console.WriteLine("PleaseenterthetemperatureinCelsius");

str=Console.ReadLine();

Int32celsius=Convert.ToInt32(str);TemperatureConversionc2f=newTemperatureConversion();

Int32fahrenheit=c2f.getFahrenheit(celsius);

Console.WriteLine("LocaltemperatureinFahrenheitis:"+fahrenheit);

}

}

classmyCost{

privateInt32days;//Datamember

publicmyCost(Int32daysToStay){//Parameterpassedintotheclassdays=daysToStay;//throughtheconstructor,whichis

}//usedtoinitializethedatamember

privateInt32hotel(){

return100*days;//Parametervalueusedinallmethods

}

privateInt32rentalCar(){

return30*days;//Parametervalueusedinallmethods

}privateInt32meals(){

return20*days;//Parametervalueusedinallmethods

}

publicInt32total(){

returnhotel()+rentalCar()+meals();

}

}

classCurrencyConversion{publicDoubleusdToLocalCurrency(Stringcountry,Int32usdAmount){

switch(country){

case"Japan":returnusdAmount*117;

case"EU":returnusdAmount*0.71;

case"HongKong":returnusdAmount*7.7;case"UK":returnusdAmount*0.49;

case"SouthAfrica":returnusdAmount*6.8;

default:return-1;

}

}

}

classTemperatureConversion{

publicInt32getFahrenheit(Int32c){

Doublef=c*9/5+32;returnConvert.ToInt32(f);

}publicInt32getCelsius(Int32f){

Doublec=(f-32)*5/9;returnConvert.ToInt32(c);

}

}

这个程序中使用的是自己编写的类。在以后的章节学习面向服务的计算时,我们将展示通过Internet访问远程对象,即所谓的Web服务,它提供实时服务,如取得当地的温度和实际汇率。2.1.6参数:用ref和out传递引用

在C#中,通过引用传递参数,或者被调用方法需要永久改变调用者的值,就要用ref关键字。看下面的例子:

usingSystem;

classPoint{

publicPoint(intx){

this.x=x;

}

publicvoidGetPoint(refintx){

x=this.x;//this.xreferstotheclassmember

}intx;

}

classTest{

publicstaticvoidMain(){

PointmyPoint=newPoint(10);intx=0;

myPoint.GetPoint(refx);//x=10

}

}

C#提供通过引用传递参数的第二种方法是用out关键字。out关键字可以传递未被引用初始化的参数。usingSystem;

classPoint{

publicPoint(intx){

this.x=x;

}

publicvoidGetPoint(outintx){

x=this.x;

}

intx;}

classTest{

publicstaticvoidMain(){

PointmyPoint=newPoint(10);

intx;

myPoint.GetPoint(outx);//x=10

}

}2.1.7基类和基类构造函数调用

C#与C++定义父类的语法相似。类只能有一个父类。定义基类和调用基类的构造函数的语法如下所示:

classCalculatorStack:stack{

publicCalculatorStack(intn):stack(n){

...//othercodehere

}

}2.1.8构造函数、析构函数和垃圾回收

与C++一样,如果程序员没有定义构造函数,C#会为每个类创建一个默认的构造函数。这保证了类的成员变量被设置为适当默认值,而不是随机指向某个位置。一个类可以定义多个构造函数。构造函数的语法包括public修饰符和带有零个或多个参数的类名。当实例化对象时,自动调用构造函数,构造函数无返回值。

通常,析构函数释放对象占用的存储空间。在C++中,当不再使用对象时,由程序员负责执行析构函数释放在堆中为对象分配的内存空间。如果没有人工清理,就会内存泄露并可能最终导致系统崩溃。C#通过.Net垃圾回收器(GC)自动清理对象并跟踪内存分配,避免了这种潜在的问题。GC具有不确定性。它不会经常运行去占用处理器的运行时间。它只在堆内存不足时运行。在一些情况下,C#程序员想手动释放资源,例如,当使用像数据库连接或者窗口句柄这样的非对象资源时。为了确保完成手动垃圾回收,Object.Finalize方法可以被重写。C#中没有delete操作符。除了这一微小的差别外,Object.Finalize方法具有和C++析构函数相同的语法和作用,如下面代码所示。

publicclassDestructorExample{

publicDestructorExample(){

Console.WriteLine(′Woohoo,objectinstantiated!′);

}~DestructorExample(){

Console.WriteLine(′Wow,destructorcalled!′);

}

}2.1.9C#中的指针

C#支持下列指针操作符,C++程序员对这些操作符已很熟悉:

(1)&:该地址运算符返回变量的内存地址。

(2)*:基本的指针运算符,用于以下两种情况:①声明一个指针变量;

②间接引用,即访问指针指向内存位置的值。

(3)>成员访问操作符,首先获取指向对象的指针,然后获得对象特定成员。

(*p).x;

p>x;

C/C++中指针的语义,以及引用和间接引用的语法,在C#中得到延续。C#的主要区别是任何使用指针的代码都需要被标记为unsafe。当生明一个unsafe方法时,要添加关键字unsafe,以标记调用了unsafe方法的代码块。在unsafe中写的程序并不是不安全的,它只是简单的允许程序员直接操纵内存,避免编译器类型检查。unsafe代码不是不被管理的代码,而仍然由实时环境和GC管理。

C#指针可以指向值类型(基本数据类型)或者引用类型。但是,你只能看到值的地址。另一个需要注意的是,如果你使用VisualStudio.Net编程,代码需要使用unsafe编译选项进行编译。

这个例子说明了C#指针的用法:

publicclassMyPointerTest{

unsafepublicstaticvoidSwap(int*xVal,int*yVal){

inttemp=*xVal;

*xVal=*yVal;*yVal=temp;

}

publicstaticvoidMain(string[]args){

intx=5;

inty=6;

Console.WriteLine("OriginalValue:x={0},y={1}",x,y);

unsafe{

Swap(&x,&y);

}Console.WriteLine("NewValue:x={0},y={1}",x,y);

}

}

控制台输出为:

OriginalValue:x=5,y=6

NewValue:x=6,y=52.1.10C#的统一类型系统

C#的统一类型系统使每一个数据类型的值都成为一个对象。引用类型(复杂类型)和值类型拥有相同的根类System.Object。通过这种方式,值类型继承了根类System.Object中的方法。下面是C#代码示例:

5.ToString()//Retrievesthenameofanobject

b.Equals()==c.Equals()//Comparestwoobjectreferencesatruntime

w.GetHashCode()//Getsthehashcodeforanobject

4.GetType()//Getsthetypeofanobject

因为所有的类型都从object派生,所以值类型可以使用点(.)操作,而不需要为值类型单独定义一个类。这样当需要像引用类型那样使用值类型时,就不必像面向对象程序员用C++(Java)编程时那样必须写一些代码以把值类型包装成一个类。

在C++中,如果你想创建一个接受任何类型的带参数的方法,你必须写一个带重载构造函数的类以支持你需要的每一个值类型。例如:

classAllTypes{public:

AllTypes(intw);

AllTypes(doublex);

AllTypes(chary);

AllTypes(shortz);

//aconstructormustbeoverloadedforeachdesiredtype

//retrievingavaluefromthisclasswouldrequireoverloadedfunctions};

classCTypesExample

{publicExample(AllTypes&myType){

}

};

在C#中,当需要引用类型却提供的是值类型时,编译器会自动对值类型装箱,并在堆中分配内存。装箱(Boxing)是编译器将一个值类型转换为引用类型的过程。反装箱(Unboxing)是将引用类型转回为值类型。

Boxing和Unboxing实例一:

intv=55;

Console.WriteLine("Valueis:{v}",v);

//Console.Writelineacceptsobjects/referencesonly

//Thecompilerwrapsvaluetypesautomatically

intv2=(int)v;//Unboxingneedscasting

Boxing和Unboxing实例二:

intv=55;

objectx=v;//explicitlyboxintvaluetypevintoreferencetypex

Console.WriteLine("Valueis:{0}",x);//Console.Writelineacceptsobjects

装箱一个变量实际上是生成一个不同的变量。如果你修改了原来的变量,被装箱的变量不会被修改。如下面的代码示例所示:

classtestBox{staticvoidMain(string[]args){

intva1=55;

objectbox=va1;//boxintvalueva1intoreferencetype

va1=va1*2;//modifytheoriginalvariable

Console.WriteLine("va1valueis:{0}",va1);//va1=55

intva2=(int)box;//unboxtheobjectandputintova2

Console.WriteLine("va2valueis:{0}",va2);//va2-110

//Adifferentvaluewillbeprinted

//intva3=box.va1;isINCORRECT:va1isNOTamemberofbox}

}

统一类型系统使得跨语言互操作成为可能。类型系统的其他优点包括保证类型安全,即在运行时追踪系统中的所有类型的一种安全机制。最终结果是通过这种在概念上建立一个更简单的编程模型的思想,使代码更安全。

当一个程序启动时,操作系统将会给程序分配一个内存段。编程语言环境(运行系统)管理分配给程序的内存。分配的内存分为三个区域:静态区、栈区、堆区,如图2.3所示。

程序员可以通过不同的方式声明变量来选择在不同的区获得内存。在C#和Java中:2.2内存管理和垃圾回收

(1)所有静态变量从静态区获取内存。换句话说,如果想为变量在静态区域获得内存,可以在变量声明前加上static,例如:

staticints声明了一个静态的整型变量。在C++中,允许使用全局变量,无论static限定是否被使用,所有全局变量(在函数外声明的变量)都可以从静态区获得内存。

(2)通过new()创建一个对象时,该对象动态从堆中获得内存。

(3)方法中的所有非静态局部变量从栈中获得内存。这个栈也被叫做程序运行时栈,以区别计算机系统中可能使用的其他栈。

图2.3分配给程序的内存的分区

现在的问题是,程序员使用不同的存储区有什么不同?这个问题将在下面一节给出答案。2.2.1静态变量和静态方法

考虑类中的静态变量。内存在编译阶段(程序执行之前)分配给静态变量。无论从类创建多少个对象,每个静态变量都只有一个内存拷贝。在一个对象中对静态变量做出改变将改变其他对象中这个变量的值。仅仅当这个程序结束时,静态变量的生命期才结束。

我们为什么需要静态局部变量?我们需要静态变量来保存被不同对象所共享的值。例如,我们可以定义一个静态变量“counter”统计一个资源被不同的对象访问多少次。下面的函数是一个用静态变量统计有多少用户成功登陆了一个访问受限区域的例子。

voidlogin(){

staticintcounter=0;//willbeinitializedonlyonce

readId_pwd();

if(verified())

counter++;//countthe#ofusersloggedin

}

我们可以声明一个静态方法。不需要创建包含静态方法的对象就可以调用静态方法,而非静态方法仅仅是在对象被创建后才可以被调用。静态方法通过className.methodName来调用,而非通过referenceName.methodName来调用。2.2.2局部变量的运行时栈

局部变量是在方法中声明的变量。当控制进入一个方法,就在栈中创建一个内存块(叫做栈区)。所有非静态局部变量从栈区获得内存。当控制离开方法,所有局部变量被释放,并且这些变量的内容不再有效(不再可以访问)。

图2.4给出了栈内存分配的示例。如图2.4左边部分所示,这个程序包含两个方法。main方法有一个局部变量i,bar方法有两个局部变量j和k。请注意,方法的形参是这个方法的局部变量。

图2.4一个简单程序及它的运行时栈

状态(0)显示了main方法执行前栈的初始状态。当控制进入main方法,局部变量i在栈顶部获得内存,如栈状态(1)所示。i的值初始化为0,然后增至1。然后i作为实参传递给bar方法,当控制进入bar方法,两个局部变量j和k获得栈顶部的内存,如状态(2)所示。i的值传递给形参j。请注意,i和j有不同的存储单元。j有i值的副本。当j在bar方法中被修改时,不影响变量i的值。

当bar函数运行完后,变量j和k生命期结束。栈指针返回bar函数运行之前的位置。变量j和k释放它们使用的内存,如栈状态(3)所示。因此我们无法在方法外访问j和k。最后,当main函数运行结束,变量i也被释放,栈指针返回到main函数运行前的位置。如图2.4状态(4)所示。

由于局部变量在它生命期结束时由运行时栈自动回收垃圾,因此程序员不需要明确地向系统返回内存。了解了如何给局部变量在栈中分配内存后,我们就很容易理解递归方法的实现。实际上,并不需要任何特殊机制。栈处理所有局部变量,也处理递归方法的变量。

让我们看看下面的fac(n)递归函数。函数中有两个局部变量:形参n和一个用来保存第(n-1)次迭代返回值的临时变量fac。图2.5显示了fac(3)递归函数执行前和执行中的运行时栈。

usingSystem;

namespacemyNamespace1

{classstackExample{

staticvoidMain(string[]args){

inti=3,j;

j=fac(i);

Console.WriteLine("j={0}",j);

}

staticintfac(intn){

if(n<=1)

return1;

elsereturnn*fac(n-1);

}

}

}

图2.5递归程序的运行时栈

状态(0)是fac(3)函数被调用前的状态。当fac(n)函数第一次执行时,两个局部变量n和fac从栈上获得内存,如状态(1)所示。形参n被初始化为实际的参数3,但是变量fac还没有值。第一次迭代中,fac(n-1)被调用,并且函数重新执行。两个局部变量再次从栈获得内存。现在,n被初始化为实际参数2,而fac没有值,如图2.5状态(2)。第二次迭代中的变量n和fac不同于第一次迭代中的n和fac,虽然它们有相同的名字,但由于它们有不同的域,所以被认为是不同的变量。

在第二次迭代中,fac(n-1)被再次调用。在这次迭代中,n被初始化为1,条件(n≤1)为真。现在fac(1)函数实际已经完成在这次迭代中把一个值返回给fac,如状态(3)所示。迭代3完成的函数在返回时调用迭代2中的fac(1),并且返回值fac=1传入迭代2。操作n*fac(1)产生一个值2,如状态(4)所示。返回值2被传给迭代1并产生一个值6,如图2.5状态(5)所示。当最后迭代完成时,fac(n)函数退出,栈指针返回初始状态(0)。

如果把这里递归方法的调用和前面例子中普通方法的调用相比较,你会发现在栈中为变量分配内存的过程是相同的。

实际上,在汇编语言(或机器代码)中,用来保存返回值的fac并不在栈中。相反地,它用的是寄存器。一个寄存器变量可被认为是一个全局变量,它对高级语言编程者是不可见的。因为寄存器概念不是高级语言编程的一部分,所以我们在这儿用栈变量fac使得值传递在栈中可见。2.2.3动态存储分配的堆

数据块的第三个区域是堆。在Java和C#中用newClassname()操作时,要求从堆中进行动态内存分配。

例如下面的代码段要求给Invoice类的对象分配内存:

classInvoice{//defineaclass

floatprice;

intphone;

};

Invoicep=newInvoice();

从堆中获得内存的数据类型叫做引用类型,因为他们的变量用内存地址(引用)作为值。2.2.4作用域和垃圾回收

到目前为止,我们解释了什么时候使用静态内存、栈内存和堆内存,也解释了怎样从静态区、栈区和堆区获得内存。最后一个需要回答的问题是,我们是否需要担心垃圾回收?换句话说,我们是否需要回收已分配过的内存?这个问题的答案取决于我们获得内存的地方和所使用的语言。

根据定义,静态变量应该存在于程序的整个生命周期(尽管它们不可见),因此永远不需要程序员或者运行时系统进行垃圾回收。当main函数运行结束,操作系统将回收所有分配给该程序的内存段。因此,如果一个变量或函数是静态的,我们就不需要担心内存的回收。

如果一个变量从栈获得内存,内存将被系统自动回收。如我们前面解释过的,当函数开始运行,局部变量或对象从栈中获得内存。当函数运行结束,栈指针移回原来的位置,分配给局部变量的内存返回给栈。这种内存回收由语言的作用域规则管理:变量的作用域从声明开始,结束于块尾。当变量超出作用域,分配给变量的内存返还给系统。

然而,如果变量或对象从堆中获得内存,就会变得很复杂。Java使用一个复杂的自动垃圾回收器回收不再用的内存。C#对管理的代码使用自动垃圾回收器。对于违背管理的代码,需要手动(显式)回收堆内存。在C++中,所有内存都要手动回收。

表2.4内存管理汇总

本节讨论多任务和多线程的一般问题,包括并行处理、同步、死锁、执行次序。这些内容是分布式计算范型的基础。2.3多任务和多线程的一般问题2.3.1基本需求

操作系统中的多任务和应用程序中的多线程类似,提供同时执行代码不同部分的能力,同时保持计算结果的正确。

在操作系统中,并行执行的代码称为进程或任务,并且它们在语义上往往相互独立(但可以有关)。操作系统进行进程调度和资源(处理器、内存、外围设备等)分配。操作系统允许用户创建、管理和同步化进程。在应用程序中,并行执行的代码叫做线程。更多的时候它们是语义相关的(但也可以独立)。

在这两种情况下,程序员必须仔细设计操作系统/应用程序以达到进程/线程同时运行而又不相互干扰,并且不管它们以什么顺序执行,在最后都输出相同结果。

我们区分程序和进程以及函数(方法)和线程。一个程序或函数(方法)是由程序员写的一段代码,它是静态的。进程或线程是由执行的程序/函数、当前值、状态信息和用于支持它执行的资源构成,资源是它执行时的动态因素。换言之,一个进程或线程是一个动态实体,只有当程序或函数执行时才会存在。

为了真正并行执行多个进程/线程,必须存在多个处理器。如果系统中只有一个处理器,表面上多个进程/线程同时执行,实际上是在分时模式下顺序执行。

从同一代码块可以创建多个进程/线程。默认情况下,包含在不同进程/线程中的代码和数据是分离的,每一个都有它自己可执行代码的副本、局部变量的栈、对象数据区以及其他数据元素。

通常情况下,一个分布式操作系统可以由不同电脑上的多个实例或副本构成,每一个实例或副本都可以管理多个进程。同样,每一个进程可以是由多个线程组成的一个多线程程序,如图2.6所示。

图2.6多进程和多线程2.3.2临界操作和同步

进程/线程可以共享资源或对象。对共享资源的访问称为临界操作,需要仔细的管理和同步化这些操作以预防出错,例如,同步读写,这可能导致不正确或不确定的结果。同步化的一个简单方式是访问资源之前加锁。图2.7显示了一个场景,两个旅行社看到了一个飞机上的同一个位子,缺乏同步导致重复预定。解决这个问题的一个简单方法是预定对象(座位)前锁定,这样其他旅行社在对象被锁定时就不能进行访问。虽然一个简单的锁定可以防止共享资源被同时访问,但也消除了并行处理的可能性。更理想的方法是不锁定并行读操作,而锁定并行读—写和写—写组合。

图2.7缺乏同步导致重复预定

图2.8显示了一个更复杂的例子,一个生产者和两个消费者线程共享一个有多个单元格的缓冲区。生产者不断将产品放入缓冲区直到缓冲区满,同时消费者不断从缓冲区获得产品直到缓冲区为空。我们怎样写一个程序模拟这个例子,使生产者和消费者并行读写缓冲区,而不引起同步问题。

图2.8生产者和消费者问题的一种解决方案我们可以用三个线程来模拟生产者和消费者,并用一个全局(静态)数组(例如整型)模拟有n个单元的缓冲区,如图2.8所示。伪代码显示了这个问题的可能的解决方案。

伪代码是否正确解决了这个问题?代码使用了一个共享变量counter,来确保当缓冲区满时(counter=N)生产者不能把产品放进缓冲区,当缓冲区为空时(counter=0)消费者不能从缓冲区取产品。这实际上是生产者—消费者问题的逻辑,它并没有解决可能发生在分布式计算系统中的同步问题。让我们看看下面的情形:

(1)counter值是5,生产者线程正在执行语句counter=counter+1;,它获得counter值,并且让counter加1使其为6。

(2)在新值被写入变量counter之前,生产者线程可能被下列原因之一打断:数值(时间片)已用完;一个更高优先级的进程到达,操作系统将处理器分配给新来的进程;发生一个异常,处理器必须处理异常。

(3)消费者线程得到了处理器,它将counter值从5减少到4。

(4)生产者线程重新获得处理器,它从被打断的点开始执行——将值6赋给counter——不正确的值赋给了counter。

让我们看看另一种情况:

(1)counter值为1,消费者线程A从缓冲区取出唯一产品。

(2)在消费者A执行“counter=counter-1”之前,线程被打断,并且消费者线程B获得处理器。它仍然看见counter值为1,因此它从缓冲区提取不存在的一个产品。事实上,缓冲区只有一个值,而这个值被读了两次,这是一个逻辑错误。

为了正确的实现多线程,我们需要恰当的使用语言提供的同步机制,这将在本章其余部分做详细讨论。2.3.3死锁和死锁的解决

分布式计算的另一个重要问题是死锁问题。死锁的情况是两个或多个竞争操作等待对方完成,导致都不能完成。一个典型的情况是:两个或多个线程执行时需要多个资源,每个线程都占有一个资源等待其他线程释放资源,如图2.9所示。

图2.9死锁场景

死锁的经典问题是哲学家就餐问题:五个哲学家都在思考和就餐。他们共享一个圆桌上的五个碗和五根筷子。思考是独立的。就餐的时候他们使用与他相邻两人共享的筷子。限定每个哲学家一次只能拿起一根筷子,如图2.10所示。

有三种技术可用来解决死锁问题:

(1)死锁预防:使用一种算法可以保证不会发生死锁。

(2)死锁避免:使用一种算法,能够预见死锁的发生从而拒绝资源请求。

(3)死锁检测和恢复:用一种算法来检测死锁的发生,强迫线程释放资源、挂起等待。

图2.10哲学家就餐问题

死锁还不是唯一问题,活锁和饥饿是两个可能发生的相关问题。

活锁是由于两个或多个线程在响应其他线程改变的状态下不断改变它们的状态而引起的。其结果是线程都不会获得资源来完成它们的任务。例如,两个线程试图同时加锁,在锁打开时,它们的睡眠时间相同,醒来后又同时试图加锁。一个类似情况是当两个人在走廊上相遇,两个人都靠边试图让另外一个人通过,但他们从一边到另一边摇摆不定,当他们试图通过时又相互挡住了对方的路。和死锁不同,死锁中的资源被占有,而活锁中资源是空闲的。为了避免活锁,应使用不同的等待时间。例如,以太网使用的CSMA/CD协议采用二进制指数退让算法来避免活锁:

(1)发送前,以太网节点侦听总线,如果总线空闲则发送。

(2)如果冲突发生,等待一个随机时间:

①第一次冲突,等待0或1时间间隙。

②第二次冲突,等待0,1,2或3时间间隙。

③第三次冲突,等待0,1,2,3,4,5,6或7时间间隙。

④第四次冲突,等待0,1,2…或15时间间隙。

⑤第n次冲突,等待0,1,2…或2n-1时间间隙。

饥饿是指一个线程在理论上能够访问到的共享资源(加锁),但实际上无法正常访问并且不能往前推进。这种情况更可能发生,当线程给了优先级,而有很多更高优先级的线程时,低优先级线程可能会饿死。其中一种解决方法是动态改变线程优先级。线程等待时间越长优先级就会变得越高。2.3.4执行顺序

线程可以代表不同的执行者,我们可能有不同的需求,比如线程的不同执行顺序。同步化能预防多个线程同时访问相同资源,但是不关心哪个线程首先访问或访问的顺序是什么。例如,我们可以定义一个顺序:在消费者从缓冲区取得产品之前生产者必须填满缓冲区。我们也可以让两个消费者轮流从缓冲区获得产品。

考虑另一个例子,我们要编写一个多线程程序来模拟乒乓球比赛。我们需要定义执行顺序,例如,A1B2A2B1A1…B1,如图2.11所示。

图2.11定义执行顺序

协调线程的执行顺序需要一个不同的机制,我们在后面的小节将进一步讨论这个问题并给出示例程序。2.3.5操作系统对多任务和多线程的支持

大多数操作系统支持多任务编程。例如,Unix系统为用户进行并行处理提供了几个系统调用:

intfork()

是unix为创建新进程提供的系统调用。当一个进程执行了fork(),就创建了一个新(子)进程,这个进程基本上是父进程的拷贝:具有相同的程序代码,包括fork()语句、状态、用户数据、系统数据段都被复制。唯一的不同是这两个进程(父和子)从系统调用fork()中返回不同的值:子进程返回值为0,而父进程的返回值是子进程的过程ID。如果父进程返回值是-1,就说明在创建子进程时产生了错误。fork()没有参数,调用者不会做错什么。错误的唯一原因是资源耗尽(例如内存不足)。在异常处理中,父进程可能需要等待一段时间(用睡眠调用)稍后再试一次。

另外在Unix系统中,fork()经常和exec(参数列表)一起使用。通常情况下,子进程返回fork()调用后会执行exec(参数列表),而父进程等待子进程终止或做其他的事情。图2.12是一个使用fork()和exec(参数列表)的典型应用。

图2.12系统调用fork()和exec()的典型应用不同版本的系统调用支持不同的参数格式。下面是系统调用的两个版本的C语言描述。

//Version1:listallparameters

intexecl(path,arg0,arg1,...,argn,null)

char*path;//path(location)ofprogramfile

char*arg0;//firstargument(programfile)

char*arg1;//secondargument

...

char*argn;//lastargumentchar*null;//nullindicatesendofarguments

//Version2:Useafilenameandanarrayastheparametersintexecvp(file,argv)

char*file;//programfilename

char*argv();//pointertothearrayofarguments

下面的C程序是一个命令行解释器(CLI)的简单设计,它等待输入一个命令,然后开始执行与命令相关的程序并把它作为自己的子进程。

#include<fcntl.h>//CommandLineInterpreter

staticvoidmain(intargc,char*argv())

{while(TRUE)

{//read,executeacommandandwaitfortermination

read_command(argv);

//readcommandnameinargv[0]anddatainargv[1]...argv[argc–1]

switch(fork())

{case–1:

printf("Cannotcreatenewprocess\n");

break;

case0:

execvp(argv[0],argv);

//Theexecvpfunctionshouldneverreturn.Ifitreturns

printf("Cannotexecute\n");//anerrormusthaveoccurred

break;

default://CLIprocessitselfwillcometothiscase

if(wait(NULL)==-1)printf("Cannotexecutewaitsystemcall\n");

//ParentprocessreceivesthePIDofchildprocessandthenwaitsfortheterminationofchild

}

}

}

这部分讨论在Java中创建和管理线程,以及线程之间的通信和同步。这部分的程序可以使用J#,.Net,Eclipse以及其他的Java编程环境执行。2.4Java中的多线程2.4.1创建和启动线程

Java编程环境通过Thread类支持多线程编程,这个类在Java标准类库中定义,它的方法有:start,run,wait,sleep,notify,notifyAll等。图2.13显示了由这些方法、事件以及系统函数如分发、超时、访问一个已锁对象变为阻塞态、对象解锁以后变为未阻塞以及完成所控制的线程状态转换。

图2.13Java线程状态转换图

Java提供两种方式创建一个新线程:继承Thread类或实现Runnable接口。

通过继承Thread类创建新线程的步骤如下:

(1)继承Thread类;

(2)覆盖run()方法;

(3)用newMyThread(...)创建一个线程;

(4)通过调用start()方法启动线程。

通过实现Runnable接口创建新线程的步骤如下:

(1)实现Runnable接口;

(2)覆盖run()方法;

(3)用newMyThread(...)创建一个线程;

(4)通过调用start()方法启动线程;

通过第一种方法创建线程,你需要定义一个Thread类的子类,在子类中必须定义一个run()方法作为线程的入口点,就像main()作为程序的入口点一样。你也可以定义多个类,每个类都有一个run()方法,但是你只能定义一个含有main()的类。下面的程序给出了一个例子,其中定义了两个线程类和一个main类。在main类中,每一个Thread类创建两个线程。线程的创建类似于对象的创建。当Thread类中的start()方法被调用后一个线程开始执行。创建和启动线程的过程类似于操作系统中进程的创建和启动,这些我们在前面章节中已经讨论过。classmyThread1extendsThread{

//othermembers

publicvoidrun(){

//dosomethinginthisthread

}

}

classmyThread2extendsThread{

//othermembers

publicvoidrun(){

//dosomethingelseinthisthread

}

}publicclassTestMyThreads{//mainclass

publicstaticvoidmain(Stringargs[]){

myThread1threadA=newmyThread1();//creatingathread

myThread1threadB=newmyThread1();

myThread2threadC=newmyThread2();

myThread2threadD=newmyThread2();

threadA.start();//startingathread

threadB.start();

threadC.start();

threadD.start();

}}

通过继承Thread类创建线程有一个限制。因为Java不支持多继承,也就是,Java类只能有一个基类,如果类已经继承了一个类,那就不能再继承Thread类。

在这种情况下,你可以声明一个实现Runnable接口的类。然后在这个类中,编写一个run()方法实现线程的功能。当创建和启动一个线程后,创建一个类的实例并作为参数传递。下面的程序给出了一个例子,这个例子对继承Thread类和实现Runnable接口这两种方式进行了说明。classmyThread1extendsThread{//UsingextendsThread

intmyID;

myThread1(intid){//constructor

myID=id;

}

publicvoidrun(){

for(inti=1;i<5;i++){

intsecond=(int)(Math.random()*500);

try{

Thread.sleep(second);

}catch(InterruptedExceptione){}

System.out.println("myThread1-id"+myID+":"+i);

}

}

}//endclassmyThread1

classmyThread2implementsRunnable{//UsingimplementsRunnable

intmyID;myThread2(intid){

myID=id;

}

publicvoidrun(){

for(intj=1;j<5;j++){

try{

Thread.sleep((int)(Math.random()*500));

}

catch(InterruptedExceptione){}

System.out.println("myThread2-id"+myID+":"+j*j);

}}

}//endclassmyThread2

publicclasstestMyThreads{

publicstaticvoidmain(String[]args){

Threadt1=newmyThread1(1);//extendsThread

Threadt2=newThread(newmyThread1(2));//implementsRunnable

Threadt3=newThread(newmyThread2(3));//implementsRunnable

Threadt4=newThread(newmyThread2(4));//implementsRunnable

t1.start();t2.start();

t3.start();

t4.start();

}

}//endtestMyThreads

从上面两个例子,我们看到,可以创建并启动一个线程的多个实例。这是线程的一个常见应用。例如,你可以定义生产者线程和消费者线程。然后,可以创建具有多个生产者和消费者的生产者—消费者问题。从一个线程类创建多个线程被称为生产线程。

可以设置线程优先级,分别用数字1~10表示。在Thread类中定义了三个常量(宏):MIN_PRIORITY,NORM_PRIORITY和MAX_PRIORITY,相应的优先级分别是1,5和10。默认情况下(不设置优先级),一个线程的优先级是NORM_PRIORITY。如果所有线程的优先级相同,在等待队列、阻塞队列和就绪队列中,它们将以先进先出的方式被服务。如果线程的优先级不同,高优先级的线程将排在低优先级线程的前面,而不论其到达时间的先后。

下面代码黑体部分显示了在四个线程启动之前对其设置不同的优先级。在线程执行过程中,打印出优先级。

classmyThread1extendsThread{//UsingextendsThread

intmyID;

myThread1(intid){//constructor

myID=id;

}

publicvoidrun(){

for(inti=1;i<5;i++){

intsecond=(int)(Math.random()*500);try{Thread.sleep(second);}

catch(InterruptedExceptione){}

System.out.print("myThread1-id"+myID+":"+i);

System.out.println("mypriorityis"+getPriority());

}

}

}//endclassmyThread1

classmyThread2extendsThread{//UsingextendsThread

intmyID;

myThread2(intid){myID=id;

}

publicvoidrun(){

for(intj=1;j<5;j++){

try{

Thread.sleep((int)(Math.random()*500));

}

catch(InterruptedExceptione){}

System.out.print("myThread2-id"

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论