如果您曾想過構建自己的超級計算機,但卻對用C語言進行並行編程望而生畏,那麼偽遠程線程可以幫您解決這一問題。這種獲獎的Java編程模型極大地簡化了集群上的並行編程,並使超級計算走出實驗室,使每一位 Java 程序員都能使用它。
在過去的三年裡,並行集群已在改變著超級計算的面貌。一旦價值數百萬美元的單體機占了主導,並行集群很快就會成為超級計算機的選擇。可以想像得到,開放源碼圈內的高漲熱情已導致產生了數百 -- 如果不是數千的話 -- 並行集群項目。第一個同時也是最著名的開放源碼集群系統是 Beowulf。在NASA 贊助下,由 Thomas Sterling和Donald Becker 在1994 年發布的Beowulf,開始是作為一個 16 節點演示集群推出的。今天,Beowulf 已有數百種實現,從Oak Ridge 國家實驗室的Stone SouperComputer到Aspen 系統公司的定制構建的商業性集群(請參閱參考資源)。
對 Java 程序員不利的是,多數集群系統都是圍繞基於C語言的軟件消息傳遞 API — 如消息傳遞接口(MPI)或並行虛擬機(PVM)— 來實現的。用C語言進行並行編程不是件容易的事,因此我設計了一個替代方案。本文將說明如何綜合運用Java 線程和 Java 遠程方法調用(RMI)來創建自己的基於Java的超級計算機。
請注意,本文假定您有 Java 線程和 RMI的應用知識。
超級計算機內有什麼?
超級計算機的定義是:由八個或更多的節點組成、作為單個高性能機器工作的集群。基於Java的超級計算機包含一個作業調度器和任意數量的運行服務器(也稱為主機)。作業調度器生成多個線程,每個線程包含執行不同子任務的代碼。各個線程將其代碼遷移到不同的運行服務器上。然後,每個運行服務器執行遷移給它的代碼並將結果返回給作業調度器。最後,作業調度器將各個線程的結果組合起來。
這種並行集群系統之所以被稱為偽遠程線程,是因為線程是在作業調度器上調度的,但線程內的代碼卻是在遠程計算機上執行的。
該系統有哪些組件?
組件一詞是指組成“偽遠程線程”並行集群系統的邏輯模塊。該系統包含以下組件:
Job dispatcher(作業調度器) 是執行控制的機器。它生成不同的線程,每個線程都包含此集群要處理的主任務的一個子任務。每個線程內的代碼都被發送到一台遠程計算機去執行。線程在作業調度器上調度,所以理論上講,該機器不應該用於執行任何子任務。
SubTask 是一個用戶定義類,該類定義主任務的一個數據或功能獨立的部分。您可以為主任務的不同部分定義不同的類。類名 SubTask 是一個示例。您可以為一個 SubTask類取任何名字,不過這個名字應該能描述分配給它的子任務。在定義SubTask類時,您必須實現JobCodeInt接口以及 jobCode()方法,下面對其進行說明。
JobCodeInt 是一個 Java接口。您必須在定義子任務的類中實現該接口和 jobCode()方法。jobCode()方法描述了將在遠程執行的代碼。如果您打算在遠程使用某個本地資源,您必須在jobCode()方法外部初始化這個資源。比方說,您要將一組圖像發送到遠程處理,就必須在jobCode()方法外部初始化 Image 對象。您可以在該方法中調用標准 Java 庫中的類,因為遠程計算機上存在這些庫。
RunServer 是一個 Java 對象,該對象允許遠程過程調用其方法。它的一個方法以實現了JobCodeInt接口的對象作為參數。 RunServer 就在運行該對象的計算機(運行服務器)上執行該對象內的代碼,並將計算結果作為 Object類的一個實例返回。Object 是 Java類層次結構中最高一級的類。
PseudoRemThr 是一個 Java類,該類封裝了一個線程並接受給定 SubTask類的一個實例。它選擇一台遠程主機,並將 SubTask 實例發送到這台主機上執行。如果您要利用某台主機上可用的特定資源(諸如數據庫或是打印機),則可以指定主機。
HostSelector 是一個模塊。如果您沒有指定遠程主機,PseudoRemThr類就會調用HostSelector 模塊來選擇特定的主機。如果沒有空閒的主機,HostSelector 會返回負載最小的遠程計算機。如果某個遠程計算機是一個多處理器系統,HostSelector 可能會不止一次地返回該主機名。目前,HostSelector 無法根據給定任務的復雜程度來選擇主機。
偽遠程線程的工作方式
要使用偽遠程線程,您必須實現作業調度器和運行服務器。本節將說明如何實現各個部分。
實現作業調度器
首先,將主任務分解為數據或功能獨立的子任務。針對每個子任務,定義一個實現JobCodeInt接口(從而實現jobCode()方法)的類。在jobCode()方法中,定義各給定子任務要執行的代碼。
請注意,您不能調用作業調度器上用戶定義的的本地資源。請在該方法外部初始化所有這類資源。例如,您可以在SubTask類的構造函數中初始化這類資源。
創建類 PseudoRemThr的若干實例,並將 SubTask的實例傳遞給 PseudoRemThr的各個實例。如果您要明確指定一台遠程主機,您可以通過調用PseudoRemThr 對象的另一個構造函數來完成。
等待這些線程完成。調用getResult()方法來獲取 PseudoRemThr的各個實例的執行結果。如果計算沒有完成,結果返回一個值為 false的Boolean 對象;否則,將返回 Object類的一個實例,其中包含了計算結果。您必須將此實例轉換為您所希望的類類型。將所有的子任務結果組合為最終結果。
實現運行服務器
實現運行服務器是一項簡單的工作:
啟動 RMI 注冊程序。
啟動 RunServer。
運行服務器在啟動時接通作業調度器,並通知作業調度器它已准備就緒,可以接受要執行的任務了。
一個計算示例
現在該測試這一模型了。以下計算示例使用兩台計算機並行運行。一台是運行 Windows 98的333 MHz Pentium II 計算機,另一台是運行 Windows 2000 專業版的500 MHz Pentium III 計算機。
為了計算從1到10^9的所有整數的平方根之和,我創建了Sqrt類,它計算dblStart和dblEnd之間所有整數的平方根之和。
Sqrt 實現JobCodeInt接口,因此也實現了jobCode()方法。在jobCode()方法中,我定義了完成這一計算的代碼。
構造函數用於將數據傳遞給 Sqrt類,並初始化作業調度器上的所有本地資源。必須將要計算其平方根之和的整數的起止點發送給構造函數。清單1 是 Sqrt類的定義
清單1. 定義Sqrt類
//Sqrt類計算dblStart和dblEnd之間的所有整數的平方根之和。
//計算在jobCode()方法內完成
//該類實現JobCodeInt接口,且實現代碼位於jobCode()方法內
//在構造函數中將數據傳遞給該類,並初始化作業調度器上的本地資源。
//本例中,要計算其平方根之和的整數序列的起止點被發送給 Sqrt類
public class Sqrt implements JobCodeInt
{
double dblStart, dblEnd, dblPartialSum;
public Sqrt(double Start,double End)
{
dblStart = Start;
dblEnd = End;
}
public Object jobCode()
{
dblPartialSum = 0;
for(double i=dblStart;i<=dblEnd;i++)
//可調用標准的Java 函數和對象。
dblPartialSum += Math.sqrt(i);
//返回結果,一個標准 Java類的對象。
return (new Double(dblPartialSum));
}
}
JobDispatcher類創建 Sqrt類的兩個實例。然後分解主任務,將一項子任務分配給一個 Sqrt 對象(Sqrt1),並將余下的子任務分配給另一個 Sqrt 對象(Sqrt2)。接下來,JobDispatcher 創建 PseudoRemThr類的兩個對象,並將 Sqrt 對象作為參數分別傳遞給它們。接下來就等待線程執行。
一旦線程執行完畢,就可從每個 PseudoRemThr 實例獲得部分結果。將各部分結果組合起來即可得到最終結果,如清單2 所示。
清單2. 工作中的JobDispatcher
//此類可以命名為您選擇的任何名稱
//這裡選用JobDispatcher只是為了方便
public class JobDispatcher
{
public static void main(String args[])
{
double fin = 10000000; //代表 10^9
double finByTen = fin/10; //代表 10^8
long nlStartTime = System.currentTimeMillis();
//范圍從1到3*10^8
Sqrt sqrt1 = new Sqrt(1,finByTen*3);
//范圍從((3*10^8)+1)到10^9
Sqrt sqrt2 = new Sqrt((finByTen*3)+1,fin);
//以下創建 PseudoRemThr類的兩個實例。 //此構造函數的參數如下所示。 //第一個參數:代表子任務的某個類的實例
//第二個參數:執行這一子任務的遠程主機
//第三個參數:PseudoRemThr 實例的描述性名稱。
PseudoRemThr psr1 = new
PseudoRemThr(sqrt1,"//192.168.1.1:3333/","Win98");
PseudoRemThr psr2 = new
PseudoRemThr(sqrt2,"//192.168.1.2:3333/","Win2K");
psr1.waitForResult(); //等待執行結束//獲取每個線程的結果
Double res1 = (Double)psr1.getResult();
Double res2 = (Double)psr2.getResult();
double finalRes = res1.doubleValue() + res2.doubleValue();
long nlEndTime = System.currentTimeMillis();
System.out.println("Total time taken: " + (nlEndTime-nlStartTime));
System.out.println("Sum: " + finalRes);
}
}
性能評價
此計算的總執行時間在120,000 毫秒到 128,000 毫秒之間。如果在不分解任務的情況下在本地運行同樣的任務,執行時間將在183,241到237,641 毫秒之間。
最初,主任務包括計算從1到10^7的所有整數的平方根之和。為測試性能,我將計算范圍擴大到 10^8,最終擴大到 10^9。
隨著任務量的增加,遠程並行執行和本地執行所需時間的差別也越來越明顯。這就是說,當執行大型任務時,遠程並行執行消耗的時間較少。遠程並行執行並不適合小型任務,因為機器間通信的系統開銷不容忽視。隨著任務量的增加,機器間通信的開銷與在單個機器上執行全部任務的開銷相比逐漸變得微不足道。因此,我得出以下結論:偽遠程線程系統能很好地完成需要進行大量計算的任務。
使用偽遠程線程有哪些優點?
因為偽遠程線程是一種基於Java的系統,它可以用於實現包含多種操作系統的集群,或異構集群。使用偽遠程線程,您就避免了轉換原有 C/C++ 代碼的麻煩,而且還能利用Java 標准庫及其各種擴展庫。此外,偽遠程線程使您不必關心內存管理。當然,其缺點就是系統性能與 JRE 性能直接相關。
發展方向
現在相當多的商業應用程序都是用Java 平台創建的,並考慮到為了利用並行性需要轉換原有的C/C++ 代碼的實際困難,現在可能是基於Java的超級計算進入商業領域的時候了。在開始創建基於Java的應用程序時就將並行性和負載均衡考慮在內是個不錯的開端。
互聯網就是異構集群的一個很好的例子,因此偽遠程線程可以在因特網中部署,將 Web 轉換為一個單一的、基於Java的超級計算機(要了解這一概念的細節,請參閱參考資源)。然而,從實際應用出發,您應注意到在一個專門執行單一任務的同構集群中將獲得最佳結果。
最後,從日常應用出發,偽遠程線程使得將局域網(LAN)-- 諸如校園網和家庭網 -- 轉換為微型的超級計算機變得相當簡單。這就是 Beowulf 系統開創的用法。有了偽遠程線程,Java編程人員也可以創建自己的超級計算機了。
參考資源
"Linux clustering cornucopia"(developerWorks, 2000 年 5 月)為您指點迷津,讓您了解當前 Linux 上可用的開放源碼集群解決方案和保密源碼集群解決方案。
要了解關於分布式操作系統的詳細信息,請查看 Andrew S. Tanenbaum的Modern Operating Systems(Prentice Hall 出版公司,1992 年 2 月)。
要了解關於並行編程的詳細信息,請參閱 Gregory V. Wilson的Practical Parallel Programming(麻省理工學院出版社,1995 年 12 月)。
要進一步理解集群,請參閱 Scalable Computing Laboratory的Cluster Cookbook。
有關運用Java 技術和 Web 進行超級計算的深層探討,請參閱 Laurence Vanhelsuw的"Create your own supercomputer with Java?"(JavaWorld, 1997 年 1 月)。
Linux Documentation Project 托管著 Beowulf HOWTO 文檔。
訪問 Beowulf 網站,以了解 Beowulf 項目的詳細信息。
請參閱關於Oak Ridge 國家實驗室著名的Stone SouperComputer的詳細信息。
Aspen 系統公司是目前提供定制集群解決方案的少數廠商之一。