面向服務(wù)的計(jì)算和web數(shù)據(jù)管理第2章 多線程分布式計(jì)算_第1頁
面向服務(wù)的計(jì)算和web數(shù)據(jù)管理第2章 多線程分布式計(jì)算_第2頁
面向服務(wù)的計(jì)算和web數(shù)據(jù)管理第2章 多線程分布式計(jì)算_第3頁
面向服務(wù)的計(jì)算和web數(shù)據(jù)管理第2章 多線程分布式計(jì)算_第4頁
面向服務(wù)的計(jì)算和web數(shù)據(jù)管理第2章 多線程分布式計(jì)算_第5頁
已閱讀5頁,還剩338頁未讀 繼續(xù)免費(fèi)閱讀

下載本文檔

版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進(jìn)行舉報(bào)或認(rèn)領(lǐng)

文檔簡介

面向服務(wù)的計(jì)算和web數(shù)據(jù)管理第2章多線程分布式計(jì)算從本章開始,我們將不僅學(xué)習(xí)概念,還學(xué)習(xí)用編程語言實(shí)現(xiàn)這些概念。我們相信通過具體編碼能夠更好的解釋這些概念以及它們的使用。我們假設(shè)讀者熟悉面向?qū)ο蟮挠?jì)算,并且用過一種面向?qū)ο缶幊陶Z言如:C++、Java或者C#編寫過面向?qū)ο蟮某绦?。但?為了使那些沒有用過C#的讀者也能理解本章的內(nèi)容,我們在本章的開始對C#和.NET作簡短的介紹。然后,轉(zhuǎn)移到多線程和多任務(wù)的一般性問題上,包括并行處理、同步、死鎖和執(zhí)行順序,這些是分布式計(jì)算范型的基礎(chǔ)。為了更好的理解所討論的概念,我們將用Java和C#開發(fā)多線程程序。最后,我們討論分布式計(jì)算異常和事件驅(qū)動編程方法。這些概念和技術(shù)被廣泛用于分布式面向服務(wù)的計(jì)算。

本章和本書中其他章節(jié)的聯(lián)系不是很緊密,因?yàn)楸菊绿岬降拇蠖鄶?shù)問題和技術(shù)都在面向服務(wù)開發(fā)環(huán)境中遇到過,因此應(yīng)用構(gòu)建者可以擺脫這些問題。如果一個(gè)服務(wù)提供者使用服務(wù)軟件,如IIS,大部分問題也都屬于服務(wù)軟件的問題。但是如果服務(wù)提供者想寫自己的服務(wù),那么學(xué)習(xí)本章的概念還是很重要的。如果讀者關(guān)注使用先進(jìn)的工具和基礎(chǔ)設(shè)施進(jìn)行面向服務(wù)的軟件開發(fā),那么可以跳過本章。

在本節(jié),我們將簡要地介紹C#。如果你熟悉C

#,可以跳過這一節(jié)。

C#和Java都是面向?qū)ο蟮木幊陶Z言,并且它們是用來寫多線程程序以及創(chuàng)建SOC服務(wù)的兩個(gè)主要編程語言。在Java中,每一個(gè)線程通過對象創(chuàng)建。在C#中,一個(gè)線程能通過對象或?qū)ο笾械暮瘮?shù)(方法)創(chuàng)建。在面向服務(wù)的應(yīng)用中,服務(wù)的組成部分用Java或C#對象定義,用開放標(biāo)準(zhǔn)接口包裝這些對象使其成為服務(wù)。2.1C#和.Net介紹

C#繼承了C/C++語言的許多語法,并支持Java的許多特征。它是一種強(qiáng)類型語言并自動進(jìn)行內(nèi)存回收。.Net通用庫中有一個(gè)很大的函數(shù)集支持C#,面向?qū)ο蠛兔嫦蚍?wù)的軟件開發(fā)人員可復(fù)用這些函數(shù)。我們假定讀者已經(jīng)掌握了Java或者C++,所以本節(jié)只對C#做簡單介紹。2.1.1C#與.Net入門

微軟的VisualStudio.Net是一種支持多種編程語言和多種編程范型的編程環(huán)境。如圖2.1所示,在第一步編譯中,高級語言的程序編譯成一種叫做中間語言(IL)的低級語言,從表面看,中間語言類似于匯編語言。中間語言程序通過公共語言運(yùn)行時(shí)(CLR)環(huán)境進(jìn)行管理和執(zhí)行。它類似于Java的字節(jié)碼,目的使CLR獨(dú)立于高級程序語言。

圖2.1微軟VisualStudio.Net編程環(huán)境

與Java環(huán)境不同的是,.Net框架與語言無關(guān)。雖然C#被認(rèn)為是它的標(biāo)志性語言,但是.Net框架并不是為某種特定語言設(shè)計(jì)的。當(dāng)開發(fā)者使用他們選擇的高級語言編寫應(yīng)用程序的時(shí)候,可以使用公共框架類庫(FCL)和環(huán)境的功能。公共框架類庫是一個(gè)通用系統(tǒng)類型,這使得它可以支持不同的編程語言。基于類型系統(tǒng),任何編程語言,例如X,都可以很容易地集成到系統(tǒng)中。唯一要做的工作是寫一個(gè)將X轉(zhuǎn)換成IL的編譯器。中間語言程序由實(shí)時(shí)(JIT)編譯器和CLR執(zhí)行,JIT編譯器采用邊使用邊編譯的策略,當(dāng)方法被第一次調(diào)用時(shí),它動態(tài)地為內(nèi)部數(shù)據(jù)結(jié)構(gòu)分配內(nèi)存塊。換句話說,JIT編譯器介于編譯和解釋執(zhí)行之間。

現(xiàn)在讓我們寫一個(gè)輸出字符串“Hello,World!”的C#程序。

在VisualStudio.Net環(huán)境下執(zhí)行這個(gè)C#程序,遵循以下步驟:

(1)從Windows的“開始”菜單啟動.Net。

(2)選擇.Net中的菜單“文件”—“新建”—“項(xiàng)目...”,一個(gè)“新項(xiàng)目”對話框彈出,在這里你可以選擇不同的編程語言,包括C/C++、J#(Java)以及C#。

(3)一旦你在框的左側(cè)選擇C#,就可以進(jìn)一步選擇一個(gè)模板幫助你開發(fā)應(yīng)用,例如:

①選擇“控制臺應(yīng)用”啟動文本和基于命令行的編程模板;

②選擇“Windows應(yīng)用”啟動一個(gè)基于表格的應(yīng)用模板,該模板允許你定義圖形用戶界面。

(4)在同一對話框的底部,你可以選擇項(xiàng)目名稱、選擇保存該項(xiàng)目的路徑、選擇方案名稱,你可以在同一個(gè)方案中放入多個(gè)工程。

(5)點(diǎn)擊“OK”。

這時(shí)一個(gè)擁有相應(yīng)庫(取決于你所選擇的模板)的項(xiàng)目模板將被創(chuàng)建。如果選擇的是C#,當(dāng)你創(chuàng)建一個(gè)新項(xiàng)目時(shí),就能夠在擴(kuò)展名為.cs的文件中輸入C#程序。

下面的程序展示了一個(gè)更復(fù)雜的例子。這個(gè)程序管理舉重比賽的得分/重量。要求用戶輸入四名運(yùn)動員的姓名和舉重重量,然后程序輸出獲勝者的姓名和舉重重量。

這個(gè)程序展示了一個(gè)典型程序中的許多重要問題,包括:輸入、輸出、字符串到整數(shù)的轉(zhuǎn)換;定義整型、字符串、整型數(shù)組的引用、字符串?dāng)?shù)組的引用、創(chuàng)建一個(gè)數(shù)組對象、循環(huán)、定義靜態(tài)函數(shù)、參數(shù)傳遞和函數(shù)調(diào)用。程序中的注釋提供了更為詳細(xì)的解釋。2.1.2C#和C++的比較

與C++相比,C#的特征更類似于Java。表2.1將C#和C++的主要特征作了對比??梢钥闯?C#向Java的自動管理方式靠攏,同時(shí)在盡可能的情況下保持C++的特征。

表2.2和表2.3分別列出了C#和C++所支持的數(shù)據(jù)類型。C/C++中一些數(shù)據(jù)類型依賴于機(jī)器,因此,這種語言標(biāo)準(zhǔn)只規(guī)定了最小范圍。例如,整數(shù)類型int的長度是16、32還是64Byte,取決于實(shí)現(xiàn)該語言的計(jì)算機(jī)體系結(jié)構(gòu)。

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

表2.2 C/C++中的數(shù)據(jù)類型

表2.3C#中的數(shù)據(jù)類型另一方面,C#運(yùn)行在虛擬機(jī).Net框架上,所有的數(shù)據(jù)類型是機(jī)器無關(guān)的。Int類型的長度總是32個(gè)字節(jié)。為了更明確,你可以使用Int32代替,特別是小數(shù)類型,它采用較少的位表示指數(shù)部分,而使用多個(gè)位表示浮點(diǎn)數(shù)的小數(shù)部分,使它成為高精度的數(shù)據(jù)類型。小數(shù)類型通常用于貨幣類的計(jì)算。

2.1.3名字空間和using指令

名字空間用于類的分組,“using<namespace>”用于引用名字空間的類,就像程序中使用庫函數(shù)一樣。下面的代碼段顯示了C#程序的基本組成。

using<namespace>//usingexistingnamespaceaslibrary

namespacemyNamespace1//definemyownnamespace

classmyclass1{

publicstaticvoidMain(){

}classmyclass2{

publicdoublePiVlue(){

}

}

C#中不存在C/C++中的頭文件,而是用名字空間引用一組類和庫來代替。代碼的第一行顯示了這種應(yīng)用。程序員可以定義自己的名字空間,如上面代碼的第二行所示,把多個(gè)類歸為一組。一個(gè)程序員在同一程序中可定義多個(gè)名字空間以防止命名沖突。例如:

namespaceVirtualStore{

namespaceCustomer{

//definecustomerclasses

classShoppingCartOrder(){...}

}

namespaceAdmin{//defineadministrationclasses

classReportGenerator(){...}

}

}

“using”指令告訴編譯器在哪里尋找程序中使用的名字空間中類的成員方法。這些成員方法的使用和庫函數(shù)一樣。例如:

usingVirtualStore;

此外,.Net框架的GUI功能、表格可以通過指令訪問:

usingSystem.Windows.Forms;

...

privateButtonButton1;//classButtonisdefinedinSystem.Windows.Forms;

指令使用的另一種形式如下所示,在程序中對每一個(gè)引用作完全限定:

privateSystem.Windows.Forms.ButtonButton1;2.1.4C#中的隊(duì)列例子

為了了解C#與C++的具體區(qū)別并編寫C#程序,我們介紹一個(gè)C#編寫的隊(duì)列例子(Chen,2006)。從這個(gè)例子可以看到,當(dāng)你學(xué)習(xí)過C++或者Java后,就能很輕松地學(xué)習(xí)C#。類、對象、構(gòu)造函數(shù)、方法的概念將在后續(xù)章節(jié)中通過示例進(jìn)一步介紹。

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);

}

}

}

程序執(zhí)行后的結(jié)果如下:

Pleaseenterthequeuesize

5

X=12Y=36Z=24

X=36Y=12Z=24

與上一節(jié)舉重的例子相比,這個(gè)例子進(jìn)一步說明了C#的特性,包括:多個(gè)類的名字空間的定義、類的構(gòu)造函數(shù)、類的繼承以及對基類構(gòu)造函數(shù)的擴(kuò)展。2.1.5C#中的類和對象

和Java一樣,所有的C#程序需要一個(gè)唯一的程序入口點(diǎn),或Main方法,作為類中的成員方法來實(shí)現(xiàn)。這不同于C++中的Main函數(shù)不屬于任何類。在C#程序中,Main函數(shù)的位置由編譯器決定,與哪個(gè)類定義Main函數(shù)無關(guān)。Main函數(shù)必須定義為靜態(tài)函數(shù),但是否接收參數(shù)或返回值是可選的。一個(gè)可選的public訪問權(quán)限修飾符通知C#編譯器任何人都可以調(diào)用這個(gè)成員方法。static關(guān)鍵字意味著調(diào)用Main函數(shù)不需要對象實(shí)例。很明顯,Main函數(shù)是第一個(gè)被執(zhí)行的方法,并且沒有其他的方法能調(diào)用Main函數(shù)。

類是一種用戶自定義類型,一種結(jié)構(gòu)化設(shè)計(jì),一種具有該類型的對象功能的描述。一個(gè)類由若干成員組成。每一個(gè)成員要么是數(shù)據(jù)成員(也叫做變量),要么是成員函數(shù)(也叫做方法)。與類同名的成員函數(shù)叫做構(gòu)造函數(shù)。構(gòu)造函數(shù)初始化類中的數(shù)據(jù)成員。其他成員函數(shù)操縱數(shù)據(jù)成員,并為其他類提供可復(fù)用的功能。

類的實(shí)例叫做對象。做個(gè)比喻,一個(gè)類可以看成是制作餅干的器具,那么對象就是該器具制作出來的餅干。當(dāng)為對象建立引用后,這個(gè)對象可通過引用訪問。用new操作符實(shí)例化一個(gè)類,就是在堆中為實(shí)例或?qū)ο蠓峙浯鎯臻g存儲其成員信息。對于所有的面向?qū)ο笳Z言,用new操作符創(chuàng)建的對象,都從堆中分配存儲空間存儲類中定義的所有成員,如變量、常量、方法。

在C#中,類的靜態(tài)成員的訪問和Java一樣,用類名和“.”操作符。<className>.<memberName>

Console.WriteLine("HelloWorld!");

其中memberName是一個(gè)方法調(diào)用或者變量名。

同Java一樣,用“.”操作符也能進(jìn)行引用對象成員的訪問。在C++中,如果引用是一個(gè)指針,那么使用指針運(yùn)算符“->”。

<referenceName>.<memberName>

time.printStandard();

其中memberName是一個(gè)方法調(diào)用或者變量名。

C++提供了第二種方式來定義對象,即簡單實(shí)例化,不需要使用new操作符。這種方法不像堆中的引用類型,而像棧中的局部類型。在C#中,“new”關(guān)鍵字是創(chuàng)建對象實(shí)例的唯一方法。

類的語法可以通過下面的語法圖描述,方括號內(nèi)是可選項(xiàng)。

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

{[classbody]

}[;]

其中,attributes被看做是內(nèi)聯(lián)注釋和聲明語句,這些語句可以添加到類、成員、參數(shù)或者其他編碼元素中。通過一個(gè)稱為reflection的庫,可以檢索這些附加的信息并在運(yùn)行時(shí)供其他代碼使用。attributes提供了把信息和聲明相關(guān)連的通用方法,在許多情況下,它是一個(gè)強(qiáng)大的工具。

modifiers表示三種訪問控制符,分別是public,protected和private,它們與C++中的訪問控制符的含義相同。如果沒有明確定義訪問權(quán)限,C#和C++都默認(rèn)為private。

還有一些其他的方法修飾符包括sealed、override、virtual,以及處理類的繼承和范圍的類修飾符abstract。

在C++中,程序員可以選擇在類聲明中定義類成員或者使用作用域操作符在類聲明外定義類成員。在C#中,所有類成員必須在類的大括號內(nèi)定義。將同一個(gè)類中相關(guān)聯(lián)的對象歸為一組的簡單思想可以設(shè)計(jì)出模塊化更好的代碼。

面向?qū)ο缶幊痰闹饕卣骶褪菍⒁粋€(gè)應(yīng)用分解成多個(gè)類。其中的一個(gè)類包含Main()方法,而其他類包含可重用的類成員和方法。

讓我們考慮一個(gè)幫助人們準(zhǔn)備旅游的程序,其中包括美元同當(dāng)?shù)厮柝泿胖g的換算,將當(dāng)?shù)販囟绒D(zhuǎn)化為華氏溫度。該程序被分解成四個(gè)類,如圖2.2所示。

圖2.2問題被分解成四個(gè)類下面給出了實(shí)現(xiàn)旅游準(zhǔn)備工作的示例代碼。myCost類中給出了帶一個(gè)參數(shù)的構(gòu)造函數(shù)。由于參數(shù)被多個(gè)成員函數(shù)使用,通過構(gòu)造函數(shù)傳遞參數(shù)就會更有效率。當(dāng)用new操作符實(shí)例化對象時(shí),該參數(shù)值被傳給對象。而其他兩個(gè)類CurrencyConversion和TemperatureConversion則不需要構(gòu)造函數(shù)。在這些類中,參數(shù)僅僅被一個(gè)成員函數(shù)使用,因此,我們可以直接將參數(shù)傳遞給成員函數(shù),而不是建立數(shù)據(jù)成員保存參數(shù)值。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);

}

}

這個(gè)程序中使用的是自己編寫的類。在以后的章節(jié)學(xué)習(xí)面向服務(wù)的計(jì)算時(shí),我們將展示通過Internet訪問遠(yuǎn)程對象,即所謂的Web服務(wù),它提供實(shí)時(shí)服務(wù),如取得當(dāng)?shù)氐臏囟群蛯?shí)際匯率。2.1.6參數(shù):用ref和out傳遞引用

在C#中,通過引用傳遞參數(shù),或者被調(diào)用方法需要永久改變調(diào)用者的值,就要用ref關(guān)鍵字。看下面的例子:

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#提供通過引用傳遞參數(shù)的第二種方法是用out關(guān)鍵字。out關(guān)鍵字可以傳遞未被引用初始化的參數(shù)。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基類和基類構(gòu)造函數(shù)調(diào)用

C#與C++定義父類的語法相似。類只能有一個(gè)父類。定義基類和調(diào)用基類的構(gòu)造函數(shù)的語法如下所示:

classCalculatorStack:stack{

publicCalculatorStack(intn):stack(n){

...//othercodehere

}

}2.1.8構(gòu)造函數(shù)、析構(gòu)函數(shù)和垃圾回收

與C++一樣,如果程序員沒有定義構(gòu)造函數(shù),C#會為每個(gè)類創(chuàng)建一個(gè)默認(rèn)的構(gòu)造函數(shù)。這保證了類的成員變量被設(shè)置為適當(dāng)默認(rèn)值,而不是隨機(jī)指向某個(gè)位置。一個(gè)類可以定義多個(gè)構(gòu)造函數(shù)。構(gòu)造函數(shù)的語法包括public修飾符和帶有零個(gè)或多個(gè)參數(shù)的類名。當(dāng)實(shí)例化對象時(shí),自動調(diào)用構(gòu)造函數(shù),構(gòu)造函數(shù)無返回值。

通常,析構(gòu)函數(shù)釋放對象占用的存儲空間。在C++中,當(dāng)不再使用對象時(shí),由程序員負(fù)責(zé)執(zhí)行析構(gòu)函數(shù)釋放在堆中為對象分配的內(nèi)存空間。如果沒有人工清理,就會內(nèi)存泄露并可能最終導(dǎo)致系統(tǒng)崩潰。C#通過.Net垃圾回收器(GC)自動清理對象并跟蹤內(nèi)存分配,避免了這種潛在的問題。GC具有不確定性。它不會經(jīng)常運(yùn)行去占用處理器的運(yùn)行時(shí)間。它只在堆內(nèi)存不足時(shí)運(yùn)行。在一些情況下,C#程序員想手動釋放資源,例如,當(dāng)使用像數(shù)據(jù)庫連接或者窗口句柄這樣的非對象資源時(shí)。為了確保完成手動垃圾回收,Object.Finalize方法可以被重寫。C#中沒有delete操作符。除了這一微小的差別外,Object.Finalize方法具有和C++析構(gòu)函數(shù)相同的語法和作用,如下面代碼所示。

publicclassDestructorExample{

publicDestructorExample(){

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

}~DestructorExample(){

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

}

}2.1.9C#中的指針

C#支持下列指針操作符,C++程序員對這些操作符已很熟悉:

(1)&:該地址運(yùn)算符返回變量的內(nèi)存地址。

(2)*:基本的指針運(yùn)算符,用于以下兩種情況:①聲明一個(gè)指針變量;

②間接引用,即訪問指針指向內(nèi)存位置的值。

(3)>成員訪問操作符,首先獲取指向?qū)ο蟮闹羔?然后獲得對象特定成員。

(*p).x;

p>x;

C/C++中指針的語義,以及引用和間接引用的語法,在C#中得到延續(xù)。C#的主要區(qū)別是任何使用指針的代碼都需要被標(biāo)記為unsafe。當(dāng)生明一個(gè)unsafe方法時(shí),要添加關(guān)鍵字unsafe,以標(biāo)記調(diào)用了unsafe方法的代碼塊。在unsafe中寫的程序并不是不安全的,它只是簡單的允許程序員直接操縱內(nèi)存,避免編譯器類型檢查。unsafe代碼不是不被管理的代碼,而仍然由實(shí)時(shí)環(huán)境和GC管理。

C#指針可以指向值類型(基本數(shù)據(jù)類型)或者引用類型。但是,你只能看到值的地址。另一個(gè)需要注意的是,如果你使用VisualStudio.Net編程,代碼需要使用unsafe編譯選項(xiàng)進(jìn)行編譯。

這個(gè)例子說明了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#的統(tǒng)一類型系統(tǒng)

C#的統(tǒng)一類型系統(tǒng)使每一個(gè)數(shù)據(jù)類型的值都成為一個(gè)對象。引用類型(復(fù)雜類型)和值類型擁有相同的根類System.Object。通過這種方式,值類型繼承了根類System.Object中的方法。下面是C#代碼示例:

5.ToString()//Retrievesthenameofanobject

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

w.GetHashCode()//Getsthehashcodeforanobject

4.GetType()//Getsthetypeofanobject

因?yàn)樗械念愋投紡膐bject派生,所以值類型可以使用點(diǎn)(.)操作,而不需要為值類型單獨(dú)定義一個(gè)類。這樣當(dāng)需要像引用類型那樣使用值類型時(shí),就不必像面向?qū)ο蟪绦騿T用C++(Java)編程時(shí)那樣必須寫一些代碼以把值類型包裝成一個(gè)類。

在C++中,如果你想創(chuàng)建一個(gè)接受任何類型的帶參數(shù)的方法,你必須寫一個(gè)帶重載構(gòu)造函數(shù)的類以支持你需要的每一個(gè)值類型。例如:

classAllTypes{public:

AllTypes(intw);

AllTypes(doublex);

AllTypes(chary);

AllTypes(shortz);

//aconstructormustbeoverloadedforeachdesiredtype

//retrievingavaluefromthisclasswouldrequireoverloadedfunctions};

classCTypesExample

{publicExample(AllTypes&myType){

}

};

在C#中,當(dāng)需要引用類型卻提供的是值類型時(shí),編譯器會自動對值類型裝箱,并在堆中分配內(nèi)存。裝箱(Boxing)是編譯器將一個(gè)值類型轉(zhuǎn)換為引用類型的過程。反裝箱(Unboxing)是將引用類型轉(zhuǎn)回為值類型。

Boxing和Unboxing實(shí)例一:

intv=55;

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

//Console.Writelineacceptsobjects/referencesonly

//Thecompilerwrapsvaluetypesautomatically

intv2=(int)v;//Unboxingneedscasting

Boxing和Unboxing實(shí)例二:

intv=55;

objectx=v;//explicitlyboxintvaluetypevintoreferencetypex

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

裝箱一個(gè)變量實(shí)際上是生成一個(gè)不同的變量。如果你修改了原來的變量,被裝箱的變量不會被修改。如下面的代碼示例所示:

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}

}

統(tǒng)一類型系統(tǒng)使得跨語言互操作成為可能。類型系統(tǒng)的其他優(yōu)點(diǎn)包括保證類型安全,即在運(yùn)行時(shí)追蹤系統(tǒng)中的所有類型的一種安全機(jī)制。最終結(jié)果是通過這種在概念上建立一個(gè)更簡單的編程模型的思想,使代碼更安全。

當(dāng)一個(gè)程序啟動時(shí),操作系統(tǒng)將會給程序分配一個(gè)內(nèi)存段。編程語言環(huán)境(運(yùn)行系統(tǒng))管理分配給程序的內(nèi)存。分配的內(nèi)存分為三個(gè)區(qū)域:靜態(tài)區(qū)、棧區(qū)、堆區(qū),如圖2.3所示。

程序員可以通過不同的方式聲明變量來選擇在不同的區(qū)獲得內(nèi)存。在C#和Java中:2.2內(nèi)存管理和垃圾回收

(1)所有靜態(tài)變量從靜態(tài)區(qū)獲取內(nèi)存。換句話說,如果想為變量在靜態(tài)區(qū)域獲得內(nèi)存,可以在變量聲明前加上static,例如:

staticints聲明了一個(gè)靜態(tài)的整型變量。在C++中,允許使用全局變量,無論static限定是否被使用,所有全局變量(在函數(shù)外聲明的變量)都可以從靜態(tài)區(qū)獲得內(nèi)存。

(2)通過new()創(chuàng)建一個(gè)對象時(shí),該對象動態(tài)從堆中獲得內(nèi)存。

(3)方法中的所有非靜態(tài)局部變量從棧中獲得內(nèi)存。這個(gè)棧也被叫做程序運(yùn)行時(shí)棧,以區(qū)別計(jì)算機(jī)系統(tǒng)中可能使用的其他棧。

圖2.3分配給程序的內(nèi)存的分區(qū)

現(xiàn)在的問題是,程序員使用不同的存儲區(qū)有什么不同?這個(gè)問題將在下面一節(jié)給出答案。2.2.1靜態(tài)變量和靜態(tài)方法

考慮類中的靜態(tài)變量。內(nèi)存在編譯階段(程序執(zhí)行之前)分配給靜態(tài)變量。無論從類創(chuàng)建多少個(gè)對象,每個(gè)靜態(tài)變量都只有一個(gè)內(nèi)存拷貝。在一個(gè)對象中對靜態(tài)變量做出改變將改變其他對象中這個(gè)變量的值。僅僅當(dāng)這個(gè)程序結(jié)束時(shí),靜態(tài)變量的生命期才結(jié)束。

我們?yōu)槭裁葱枰o態(tài)局部變量?我們需要靜態(tài)變量來保存被不同對象所共享的值。例如,我們可以定義一個(gè)靜態(tài)變量“counter”統(tǒng)計(jì)一個(gè)資源被不同的對象訪問多少次。下面的函數(shù)是一個(gè)用靜態(tài)變量統(tǒng)計(jì)有多少用戶成功登陸了一個(gè)訪問受限區(qū)域的例子。

voidlogin(){

staticintcounter=0;//willbeinitializedonlyonce

readId_pwd();

if(verified())

counter++;//countthe#ofusersloggedin

}

我們可以聲明一個(gè)靜態(tài)方法。不需要創(chuàng)建包含靜態(tài)方法的對象就可以調(diào)用靜態(tài)方法,而非靜態(tài)方法僅僅是在對象被創(chuàng)建后才可以被調(diào)用。靜態(tài)方法通過className.methodName來調(diào)用,而非通過referenceName.methodName來調(diào)用。2.2.2局部變量的運(yùn)行時(shí)棧

局部變量是在方法中聲明的變量。當(dāng)控制進(jìn)入一個(gè)方法,就在棧中創(chuàng)建一個(gè)內(nèi)存塊(叫做棧區(qū))。所有非靜態(tài)局部變量從棧區(qū)獲得內(nèi)存。當(dāng)控制離開方法,所有局部變量被釋放,并且這些變量的內(nèi)容不再有效(不再可以訪問)。

圖2.4給出了棧內(nèi)存分配的示例。如圖2.4左邊部分所示,這個(gè)程序包含兩個(gè)方法。main方法有一個(gè)局部變量i,bar方法有兩個(gè)局部變量j和k。請注意,方法的形參是這個(gè)方法的局部變量。

圖2.4一個(gè)簡單程序及它的運(yùn)行時(shí)棧

狀態(tài)(0)顯示了main方法執(zhí)行前棧的初始狀態(tài)。當(dāng)控制進(jìn)入main方法,局部變量i在棧頂部獲得內(nèi)存,如棧狀態(tài)(1)所示。i的值初始化為0,然后增至1。然后i作為實(shí)參傳遞給bar方法,當(dāng)控制進(jìn)入bar方法,兩個(gè)局部變量j和k獲得棧頂部的內(nèi)存,如狀態(tài)(2)所示。i的值傳遞給形參j。請注意,i和j有不同的存儲單元。j有i值的副本。當(dāng)j在bar方法中被修改時(shí),不影響變量i的值。

當(dāng)bar函數(shù)運(yùn)行完后,變量j和k生命期結(jié)束。棧指針返回bar函數(shù)運(yùn)行之前的位置。變量j和k釋放它們使用的內(nèi)存,如棧狀態(tài)(3)所示。因此我們無法在方法外訪問j和k。最后,當(dāng)main函數(shù)運(yùn)行結(jié)束,變量i也被釋放,棧指針返回到main函數(shù)運(yùn)行前的位置。如圖2.4狀態(tài)(4)所示。

由于局部變量在它生命期結(jié)束時(shí)由運(yùn)行時(shí)棧自動回收垃圾,因此程序員不需要明確地向系統(tǒng)返回內(nèi)存。了解了如何給局部變量在棧中分配內(nèi)存后,我們就很容易理解遞歸方法的實(shí)現(xiàn)。實(shí)際上,并不需要任何特殊機(jī)制。棧處理所有局部變量,也處理遞歸方法的變量。

讓我們看看下面的fac(n)遞歸函數(shù)。函數(shù)中有兩個(gè)局部變量:形參n和一個(gè)用來保存第(n-1)次迭代返回值的臨時(shí)變量fac。圖2.5顯示了fac(3)遞歸函數(shù)執(zhí)行前和執(zhí)行中的運(yùn)行時(shí)棧。

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遞歸程序的運(yùn)行時(shí)棧

狀態(tài)(0)是fac(3)函數(shù)被調(diào)用前的狀態(tài)。當(dāng)fac(n)函數(shù)第一次執(zhí)行時(shí),兩個(gè)局部變量n和fac從棧上獲得內(nèi)存,如狀態(tài)(1)所示。形參n被初始化為實(shí)際的參數(shù)3,但是變量fac還沒有值。第一次迭代中,fac(n-1)被調(diào)用,并且函數(shù)重新執(zhí)行。兩個(gè)局部變量再次從棧獲得內(nèi)存?,F(xiàn)在,n被初始化為實(shí)際參數(shù)2,而fac沒有值,如圖2.5狀態(tài)(2)。第二次迭代中的變量n和fac不同于第一次迭代中的n和fac,雖然它們有相同的名字,但由于它們有不同的域,所以被認(rèn)為是不同的變量。

在第二次迭代中,fac(n-1)被再次調(diào)用。在這次迭代中,n被初始化為1,條件(n≤1)為真?,F(xiàn)在fac(1)函數(shù)實(shí)際已經(jīng)完成在這次迭代中把一個(gè)值返回給fac,如狀態(tài)(3)所示。迭代3完成的函數(shù)在返回時(shí)調(diào)用迭代2中的fac(1),并且返回值fac=1傳入迭代2。操作n*fac(1)產(chǎn)生一個(gè)值2,如狀態(tài)(4)所示。返回值2被傳給迭代1并產(chǎn)生一個(gè)值6,如圖2.5狀態(tài)(5)所示。當(dāng)最后迭代完成時(shí),fac(n)函數(shù)退出,棧指針返回初始狀態(tài)(0)。

如果把這里遞歸方法的調(diào)用和前面例子中普通方法的調(diào)用相比較,你會發(fā)現(xiàn)在棧中為變量分配內(nèi)存的過程是相同的。

實(shí)際上,在匯編語言(或機(jī)器代碼)中,用來保存返回值的fac并不在棧中。相反地,它用的是寄存器。一個(gè)寄存器變量可被認(rèn)為是一個(gè)全局變量,它對高級語言編程者是不可見的。因?yàn)榧拇嫫鞲拍畈皇歉呒壵Z言編程的一部分,所以我們在這兒用棧變量fac使得值傳遞在棧中可見。2.2.3動態(tài)存儲分配的堆

數(shù)據(jù)塊的第三個(gè)區(qū)域是堆。在Java和C#中用newClassname()操作時(shí),要求從堆中進(jìn)行動態(tài)內(nèi)存分配。

例如下面的代碼段要求給Invoice類的對象分配內(nèi)存:

classInvoice{//defineaclass

floatprice;

intphone;

};

Invoicep=newInvoice();

從堆中獲得內(nèi)存的數(shù)據(jù)類型叫做引用類型,因?yàn)樗麄兊淖兞坑脙?nèi)存地址(引用)作為值。2.2.4作用域和垃圾回收

到目前為止,我們解釋了什么時(shí)候使用靜態(tài)內(nèi)存、棧內(nèi)存和堆內(nèi)存,也解釋了怎樣從靜態(tài)區(qū)、棧區(qū)和堆區(qū)獲得內(nèi)存。最后一個(gè)需要回答的問題是,我們是否需要擔(dān)心垃圾回收?換句話說,我們是否需要回收已分配過的內(nèi)存?這個(gè)問題的答案取決于我們獲得內(nèi)存的地方和所使用的語言。

根據(jù)定義,靜態(tài)變量應(yīng)該存在于程序的整個(gè)生命周期(盡管它們不可見),因此永遠(yuǎn)不需要程序員或者運(yùn)行時(shí)系統(tǒng)進(jìn)行垃圾回收。當(dāng)main函數(shù)運(yùn)行結(jié)束,操作系統(tǒng)將回收所有分配給該程序的內(nèi)存段。因此,如果一個(gè)變量或函數(shù)是靜態(tài)的,我們就不需要擔(dān)心內(nèi)存的回收。

如果一個(gè)變量從棧獲得內(nèi)存,內(nèi)存將被系統(tǒng)自動回收。如我們前面解釋過的,當(dāng)函數(shù)開始運(yùn)行,局部變量或?qū)ο髲臈V蝎@得內(nèi)存。當(dāng)函數(shù)運(yùn)行結(jié)束,棧指針移回原來的位置,分配給局部變量的內(nèi)存返回給棧。這種內(nèi)存回收由語言的作用域規(guī)則管理:變量的作用域從聲明開始,結(jié)束于塊尾。當(dāng)變量超出作用域,分配給變量的內(nèi)存返還給系統(tǒng)。

然而,如果變量或?qū)ο髲亩阎蝎@得內(nèi)存,就會變得很復(fù)雜。Java使用一個(gè)復(fù)雜的自動垃圾回收器回收不再用的內(nèi)存。C#對管理的代碼使用自動垃圾回收器。對于違背管理的代碼,需要手動(顯式)回收堆內(nèi)存。在C++中,所有內(nèi)存都要手動回收。

表2.4內(nèi)存管理匯總

本節(jié)討論多任務(wù)和多線程的一般問題,包括并行處理、同步、死鎖、執(zhí)行次序。這些內(nèi)容是分布式計(jì)算范型的基礎(chǔ)。2.3多任務(wù)和多線程的一般問題2.3.1基本需求

操作系統(tǒng)中的多任務(wù)和應(yīng)用程序中的多線程類似,提供同時(shí)執(zhí)行代碼不同部分的能力,同時(shí)保持計(jì)算結(jié)果的正確。

在操作系統(tǒng)中,并行執(zhí)行的代碼稱為進(jìn)程或任務(wù),并且它們在語義上往往相互獨(dú)立(但可以有關(guān))。操作系統(tǒng)進(jìn)行進(jìn)程調(diào)度和資源(處理器、內(nèi)存、外圍設(shè)備等)分配。操作系統(tǒng)允許用戶創(chuàng)建、管理和同步化進(jìn)程。在應(yīng)用程序中,并行執(zhí)行的代碼叫做線程。更多的時(shí)候它們是語義相關(guān)的(但也可以獨(dú)立)。

在這兩種情況下,程序員必須仔細(xì)設(shè)計(jì)操作系統(tǒng)/應(yīng)用程序以達(dá)到進(jìn)程/線程同時(shí)運(yùn)行而又不相互干擾,并且不管它們以什么順序執(zhí)行,在最后都輸出相同結(jié)果。

我們區(qū)分程序和進(jìn)程以及函數(shù)(方法)和線程。一個(gè)程序或函數(shù)(方法)是由程序員寫的一段代碼,它是靜態(tài)的。進(jìn)程或線程是由執(zhí)行的程序/函數(shù)、當(dāng)前值、狀態(tài)信息和用于支持它執(zhí)行的資源構(gòu)成,資源是它執(zhí)行時(shí)的動態(tài)因素。換言之,一個(gè)進(jìn)程或線程是一個(gè)動態(tài)實(shí)體,只有當(dāng)程序或函數(shù)執(zhí)行時(shí)才會存在。

為了真正并行執(zhí)行多個(gè)進(jìn)程/線程,必須存在多個(gè)處理器。如果系統(tǒng)中只有一個(gè)處理器,表面上多個(gè)進(jìn)程/線程同時(shí)執(zhí)行,實(shí)際上是在分時(shí)模式下順序執(zhí)行。

從同一代碼塊可以創(chuàng)建多個(gè)進(jìn)程/線程。默認(rèn)情況下,包含在不同進(jìn)程/線程中的代碼和數(shù)據(jù)是分離的,每一個(gè)都有它自己可執(zhí)行代碼的副本、局部變量的棧、對象數(shù)據(jù)區(qū)以及其他數(shù)據(jù)元素。

通常情況下,一個(gè)分布式操作系統(tǒng)可以由不同電腦上的多個(gè)實(shí)例或副本構(gòu)成,每一個(gè)實(shí)例或副本都可以管理多個(gè)進(jìn)程。同樣,每一個(gè)進(jìn)程可以是由多個(gè)線程組成的一個(gè)多線程程序,如圖2.6所示。

圖2.6多進(jìn)程和多線程2.3.2臨界操作和同步

進(jìn)程/線程可以共享資源或?qū)ο?。對共享資源的訪問稱為臨界操作,需要仔細(xì)的管理和同步化這些操作以預(yù)防出錯(cuò),例如,同步讀寫,這可能導(dǎo)致不正確或不確定的結(jié)果。同步化的一個(gè)簡單方式是訪問資源之前加鎖。圖2.7顯示了一個(gè)場景,兩個(gè)旅行社看到了一個(gè)飛機(jī)上的同一個(gè)位子,缺乏同步導(dǎo)致重復(fù)預(yù)定。解決這個(gè)問題的一個(gè)簡單方法是預(yù)定對象(座位)前鎖定,這樣其他旅行社在對象被鎖定時(shí)就不能進(jìn)行訪問。雖然一個(gè)簡單的鎖定可以防止共享資源被同時(shí)訪問,但也消除了并行處理的可能性。更理想的方法是不鎖定并行讀操作,而鎖定并行讀—寫和寫—寫組合。

圖2.7缺乏同步導(dǎo)致重復(fù)預(yù)定

圖2.8顯示了一個(gè)更復(fù)雜的例子,一個(gè)生產(chǎn)者和兩個(gè)消費(fèi)者線程共享一個(gè)有多個(gè)單元格的緩沖區(qū)。生產(chǎn)者不斷將產(chǎn)品放入緩沖區(qū)直到緩沖區(qū)滿,同時(shí)消費(fèi)者不斷從緩沖區(qū)獲得產(chǎn)品直到緩沖區(qū)為空。我們怎樣寫一個(gè)程序模擬這個(gè)例子,使生產(chǎn)者和消費(fèi)者并行讀寫緩沖區(qū),而不引起同步問題。

圖2.8生產(chǎn)者和消費(fèi)者問題的一種解決方案我們可以用三個(gè)線程來模擬生產(chǎn)者和消費(fèi)者,并用一個(gè)全局(靜態(tài))數(shù)組(例如整型)模擬有n個(gè)單元的緩沖區(qū),如圖2.8所示。偽代碼顯示了這個(gè)問題的可能的解決方案。

偽代碼是否正確解決了這個(gè)問題?代碼使用了一個(gè)共享變量counter,來確保當(dāng)緩沖區(qū)滿時(shí)(counter=N)生產(chǎn)者不能把產(chǎn)品放進(jìn)緩沖區(qū),當(dāng)緩沖區(qū)為空時(shí)(counter=0)消費(fèi)者不能從緩沖區(qū)取產(chǎn)品。這實(shí)際上是生產(chǎn)者—消費(fèi)者問題的邏輯,它并沒有解決可能發(fā)生在分布式計(jì)算系統(tǒng)中的同步問題。讓我們看看下面的情形:

(1)counter值是5,生產(chǎn)者線程正在執(zhí)行語句counter=counter+1;,它獲得counter值,并且讓counter加1使其為6。

(2)在新值被寫入變量counter之前,生產(chǎn)者線程可能被下列原因之一打斷:數(shù)值(時(shí)間片)已用完;一個(gè)更高優(yōu)先級的進(jìn)程到達(dá),操作系統(tǒng)將處理器分配給新來的進(jìn)程;發(fā)生一個(gè)異常,處理器必須處理異常。

(3)消費(fèi)者線程得到了處理器,它將counter值從5減少到4。

(4)生產(chǎn)者線程重新獲得處理器,它從被打斷的點(diǎn)開始執(zhí)行——將值6賦給counter——不正確的值賦給了counter。

讓我們看看另一種情況:

(1)counter值為1,消費(fèi)者線程A從緩沖區(qū)取出唯一產(chǎn)品。

(2)在消費(fèi)者A執(zhí)行“counter=counter-1”之前,線程被打斷,并且消費(fèi)者線程B獲得處理器。它仍然看見counter值為1,因此它從緩沖區(qū)提取不存在的一個(gè)產(chǎn)品。事實(shí)上,緩沖區(qū)只有一個(gè)值,而這個(gè)值被讀了兩次,這是一個(gè)邏輯錯(cuò)誤。

為了正確的實(shí)現(xiàn)多線程,我們需要恰當(dāng)?shù)氖褂谜Z言提供的同步機(jī)制,這將在本章其余部分做詳細(xì)討論。2.3.3死鎖和死鎖的解決

分布式計(jì)算的另一個(gè)重要問題是死鎖問題。死鎖的情況是兩個(gè)或多個(gè)競爭操作等待對方完成,導(dǎo)致都不能完成。一個(gè)典型的情況是:兩個(gè)或多個(gè)線程執(zhí)行時(shí)需要多個(gè)資源,每個(gè)線程都占有一個(gè)資源等待其他線程釋放資源,如圖2.9所示。

圖2.9死鎖場景

死鎖的經(jīng)典問題是哲學(xué)家就餐問題:五個(gè)哲學(xué)家都在思考和就餐。他們共享一個(gè)圓桌上的五個(gè)碗和五根筷子。思考是獨(dú)立的。就餐的時(shí)候他們使用與他相鄰兩人共享的筷子。限定每個(gè)哲學(xué)家一次只能拿起一根筷子,如圖2.10所示。

有三種技術(shù)可用來解決死鎖問題:

(1)死鎖預(yù)防:使用一種算法可以保證不會發(fā)生死鎖。

(2)死鎖避免:使用一種算法,能夠預(yù)見死鎖的發(fā)生從而拒絕資源請求。

(3)死鎖檢測和恢復(fù):用一種算法來檢測死鎖的發(fā)生,強(qiáng)迫線程釋放資源、掛起等待。

圖2.10哲學(xué)家就餐問題

死鎖還不是唯一問題,活鎖和饑餓是兩個(gè)可能發(fā)生的相關(guān)問題。

活鎖是由于兩個(gè)或多個(gè)線程在響應(yīng)其他線程改變的狀態(tài)下不斷改變它們的狀態(tài)而引起的。其結(jié)果是線程都不會獲得資源來完成它們的任務(wù)。例如,兩個(gè)線程試圖同時(shí)加鎖,在鎖打開時(shí),它們的睡眠時(shí)間相同,醒來后又同時(shí)試圖加鎖。一個(gè)類似情況是當(dāng)兩個(gè)人在走廊上相遇,兩個(gè)人都靠邊試圖讓另外一個(gè)人通過,但他們從一邊到另一邊搖擺不定,當(dāng)他們試圖通過時(shí)又相互擋住了對方的路。和死鎖不同,死鎖中的資源被占有,而活鎖中資源是空閑的。為了避免活鎖,應(yīng)使用不同的等待時(shí)間。例如,以太網(wǎng)使用的CSMA/CD協(xié)議采用二進(jìn)制指數(shù)退讓算法來避免活鎖:

(1)發(fā)送前,以太網(wǎng)節(jié)點(diǎn)偵聽總線,如果總線空閑則發(fā)送。

(2)如果沖突發(fā)生,等待一個(gè)隨機(jī)時(shí)間:

①第一次沖突,等待0或1時(shí)間間隙。

②第二次沖突,等待0,1,2或3時(shí)間間隙。

③第三次沖突,等待0,1,2,3,4,5,6或7時(shí)間間隙。

④第四次沖突,等待0,1,2…或15時(shí)間間隙。

⑤第n次沖突,等待0,1,2…或2n-1時(shí)間間隙。

饑餓是指一個(gè)線程在理論上能夠訪問到的共享資源(加鎖),但實(shí)際上無法正常訪問并且不能往前推進(jìn)。這種情況更可能發(fā)生,當(dāng)線程給了優(yōu)先級,而有很多更高優(yōu)先級的線程時(shí),低優(yōu)先級線程可能會餓死。其中一種解決方法是動態(tài)改變線程優(yōu)先級。線程等待時(shí)間越長優(yōu)先級就會變得越高。2.3.4執(zhí)行順序

線程可以代表不同的執(zhí)行者,我們可能有不同的需求,比如線程的不同執(zhí)行順序。同步化能預(yù)防多個(gè)線程同時(shí)訪問相同資源,但是不關(guān)心哪個(gè)線程首先訪問或訪問的順序是什么。例如,我們可以定義一個(gè)順序:在消費(fèi)者從緩沖區(qū)取得產(chǎn)品之前生產(chǎn)者必須填滿緩沖區(qū)。我們也可以讓兩個(gè)消費(fèi)者輪流從緩沖區(qū)獲得產(chǎn)品。

考慮另一個(gè)例子,我們要編寫一個(gè)多線程程序來模擬乒乓球比賽。我們需要定義執(zhí)行順序,例如,A1B2A2B1A1…B1,如圖2.11所示。

圖2.11定義執(zhí)行順序

協(xié)調(diào)線程的執(zhí)行順序需要一個(gè)不同的機(jī)制,我們在后面的小節(jié)將進(jìn)一步討論這個(gè)問題并給出示例程序。2.3.5操作系統(tǒng)對多任務(wù)和多線程的支持

大多數(shù)操作系統(tǒng)支持多任務(wù)編程。例如,Unix系統(tǒng)為用戶進(jìn)行并行處理提供了幾個(gè)系統(tǒng)調(diào)用:

intfork()

是unix為創(chuàng)建新進(jìn)程提供的系統(tǒng)調(diào)用。當(dāng)一個(gè)進(jìn)程執(zhí)行了fork(),就創(chuàng)建了一個(gè)新(子)進(jìn)程,這個(gè)進(jìn)程基本上是父進(jìn)程的拷貝:具有相同的程序代碼,包括fork()語句、狀態(tài)、用戶數(shù)據(jù)、系統(tǒng)數(shù)據(jù)段都被復(fù)制。唯一的不同是這兩個(gè)進(jìn)程(父和子)從系統(tǒng)調(diào)用fork()中返回不同的值:子進(jìn)程返回值為0,而父進(jìn)程的返回值是子進(jìn)程的過程ID。如果父進(jìn)程返回值是-1,就說明在創(chuàng)建子進(jìn)程時(shí)產(chǎn)生了錯(cuò)誤。fork()沒有參數(shù),調(diào)用者不會做錯(cuò)什么。錯(cuò)誤的唯一原因是資源耗盡(例如內(nèi)存不足)。在異常處理中,父進(jìn)程可能需要等待一段時(shí)間(用睡眠調(diào)用)稍后再試一次。

另外在Unix系統(tǒng)中,fork()經(jīng)常和exec(參數(shù)列表)一起使用。通常情況下,子進(jìn)程返回fork()調(diào)用后會執(zhí)行exec(參數(shù)列表),而父進(jìn)程等待子進(jìn)程終止或做其他的事情。圖2.12是一個(gè)使用fork()和exec(參數(shù)列表)的典型應(yīng)用。

圖2.12系統(tǒng)調(diào)用fork()和exec()的典型應(yīng)用不同版本的系統(tǒng)調(diào)用支持不同的參數(shù)格式。下面是系統(tǒng)調(diào)用的兩個(gè)版本的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程序是一個(gè)命令行解釋器(CLI)的簡單設(shè)計(jì),它等待輸入一個(gè)命令,然后開始執(zhí)行與命令相關(guān)的程序并把它作為自己的子進(jìn)程。

#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中創(chuàng)建和管理線程,以及線程之間的通信和同步。這部分的程序可以使用J#,.Net,Eclipse以及其他的Java編程環(huán)境執(zhí)行。2.4Java中的多線程2.4.1創(chuàng)建和啟動線程

Java編程環(huán)境通過Thread類支持多線程編程,這個(gè)類在Java標(biāo)準(zhǔn)類庫中定義,它的方法有:start,run,wait,sleep,notify,notifyAll等。圖2.13顯示了由這些方法、事件以及系統(tǒng)函數(shù)如分發(fā)、超時(shí)、訪問一個(gè)已鎖對象變?yōu)樽枞麘B(tài)、對象解鎖以后變?yōu)槲醋枞约巴瓿伤刂频木€程狀態(tài)轉(zhuǎn)換。

圖2.13Java線程狀態(tài)轉(zhuǎn)換圖

Java提供兩種方式創(chuàng)建一個(gè)新線程:繼承Thread類或?qū)崿F(xiàn)Runnable接口。

通過繼承Thread類創(chuàng)建新線程的步驟如下:

(1)繼承Thread類;

(2)覆蓋run()方法;

(3)用newMyThread(...)創(chuàng)建一個(gè)線程;

(4)通過調(diào)用start()方法啟動線程。

通過實(shí)現(xiàn)Runnable接口創(chuàng)建新線程的步驟如下:

(1)實(shí)現(xiàn)Runnable接口;

(2)覆蓋run()方法;

(3)用newMyThread(...)創(chuàng)建一個(gè)線程;

(4)通過調(diào)用start()方法啟動線程;

通過第一種方法創(chuàng)建線程,你需要定義一個(gè)Thread類的子類,在子類中必須定義一個(gè)run()方法作為線程的入口點(diǎn),就像main()作為程序的入口點(diǎn)一樣。你也可以定義多個(gè)類,每個(gè)類都有一個(gè)run()方法,但是你只能定義一個(gè)含有main()的類。下面的程序給出了一個(gè)例子,其中定義了兩個(gè)線程類和一個(gè)main類。在main類中,每一個(gè)Thread類創(chuàng)建兩個(gè)線程。線程的創(chuàng)建類似于對象的創(chuàng)建。當(dāng)Thread類中的start()方法被調(diào)用后一個(gè)線程開始執(zhí)行。創(chuàng)建和啟動線程的過程類似于操作系統(tǒng)中進(jìn)程的創(chuàng)建和啟動,這些我們在前面章節(jié)中已經(jīng)討論過。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類創(chuàng)建線程有一個(gè)限制。因?yàn)镴ava不支持多繼承,也就是,Java類只能有一個(gè)基類,如果類已經(jīng)繼承了一個(gè)類,那就不能再繼承Thread類。

在這種情況下,你可以聲明一個(gè)實(shí)現(xiàn)Runnable接口的類。然后在這個(gè)類中,編寫一個(gè)run()方法實(shí)現(xiàn)線程的功能。當(dāng)創(chuàng)建和啟動一個(gè)線程后,創(chuàng)建一個(gè)類的實(shí)例并作為參數(shù)傳遞。下面的程序給出了一個(gè)例子,這個(gè)例子對繼承Thread類和實(shí)現(xiàn)Runnable接口這兩種方式進(jìn)行了說明。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

從上面兩個(gè)例子,我們看到,可以創(chuàng)建并啟動一個(gè)線程的多個(gè)實(shí)例。這是線程的一個(gè)常見應(yīng)用。例如,你可以定義生產(chǎn)者線程和消費(fèi)者線程。然后,可以創(chuàng)建具有多個(gè)生產(chǎn)者和消費(fèi)者的生產(chǎn)者—消費(fèi)者問題。從一個(gè)線程類創(chuàng)建多個(gè)線程被稱為生產(chǎn)線程。

可以設(shè)置線程優(yōu)先級,分別用數(shù)字1~10表示。在Thread類中定義了三個(gè)常量(宏):MIN_PRIORITY,NORM_PRIORITY和MAX_PRIORITY,相應(yīng)的優(yōu)先級分別是1,5和10。默認(rèn)情況下(不設(shè)置優(yōu)先級),一個(gè)線程的優(yōu)先級是NORM_PRIORITY。如果所有線程的優(yōu)先級相同,在等待隊(duì)列、阻塞隊(duì)列和就緒隊(duì)列中,它們將以先進(jìn)先出的方式被服務(wù)。如果線程的優(yōu)先級不同,高優(yōu)先級的線程將排在低優(yōu)先級線程的前面,而不論其到達(dá)時(shí)間的先后。

下面代碼黑體部分顯示了在四個(gè)線程啟動之前對其設(shè)置不同的優(yōu)先級。在線程執(zhí)行過程中,打印出優(yōu)先級。

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. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
  • 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負(fù)責(zé)。
  • 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時(shí)也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論