現在,由於計算機系統已經從人機交互逐步向機機交互轉化,計算機和計算機之間的業務對於時間的要求非常高。軟件系統對於業務的支持已經不僅表現為對不同業務的邏輯和數據(算法+數據結構)支持,而且還表現為對同時處理不同任務的時效性(任務響應速度)支持。一般,任務響應的速度可以通過算法優化及並行運算分擔負載等手段來提高。但是,用戶業務邏輯的復雜度決定了算法優化的發揮空間,硬件規模決定了所能夠承擔負載的大小。我們利用Java平台的特點,借鑒協調式多任務思想,使CPU資源能夠在任務間動態分配,從而為時間要求強的任務分配更多的CPU運行資源。這也可以充分利用現有硬件,為用戶業務提供最大的保障。
用Java解決問題
本著軟件系統結構和現實系統結構一致的思想,開發復雜業務服務的程序一般按照計算機任務和現實業務對應的思路,最終形成一個大規模的多任務系統。由於其跨平台性,Java系統可以隨著業務的擴大,平滑地升級到各種硬件平台上。由於Java自身的發展及其應用場合的不斷擴大,用它實現多任務系統已經成為當前的應用方向。在J2EE(Java2 Enterprise Edition)推出以後,Sun公司已經將Java的重心放在了服務器端(Server Side)系統的構造上。由於客戶/服務器模型固有的多對一的關系,服務器端程序也必然是一個多任務系統。
在Java多任務應用中,動態地將CPU資源在任務間分配有很重要的意義。比如一個Internet服務商的系統往往有多種任務同時運行,有HTTP、FTP、MAIL等協議的支持,也有商務、娛樂、生活、咨詢等業務的服務。在白天,網站希望系統的CPU資源盡量保障網上用戶的服務質量,提高電子商務等任務的響應速度;晚上則希望讓自己的娛樂服務和資料下載盡可能滿足下班後人們的需要。另外,在新興的網管(比如TMN, Telecommunication Management Network)等應用領域中,服務程序往往需要支持成千上萬個並發響應事件的被管理對象(MO,Managed Object)。對於被管理對象執行的操作,不同用戶在不同時刻往往有不同的時間要求。
方案選擇
在考慮動態分配CPU資源的實施方案時,往往有以下兩點要求:
1. 須充分利用現有硬件資源,在系統空閒時,讓低優先級任務也能夠得到系統所能給予的最快響應。
2.當硬件資源超負荷運行時,雖然系統中有大規模、多數量的任務不能處理,但它不應受影響,而能夠順利處理那些能夠被處理的、最重要的高優先級任務。
多任務系統要用多線程實現的最簡單方法就是將線程和任務一一對應,動態調整線程的優先級,利用線程調度來完成CPU資源在不同任務間動態分配。這種思路在以前使用本地化代碼(Native Code),充分利用特定硬件和操作系統技巧的基礎上是基本可行的。但在跨平台的Java環境中,這個思路對僅有小規模任務數的簡單系統才可行,原因有以下兩點:
1. Java的線程雖然在編程角度(API)是與平台無關的,但它的運行效果卻和不同操作系統平台密切相關。為了利用更多的CPU資源,Java中的一個線程(Thread)就對應著不同操作系統下的一個真實線程。因為Java虛擬機沒有實現線程的調度,所以這些Java的線程在不同操作系統調度下運行的差異性也就比較明顯。例如在Windows系統中,不僅線程的優先級少於Java API參數規定的十個優先級,而且微軟明確反對程序員動態調整線程優先級。即使在操作系統中有足夠的優先權,讓線程優先級的參數和真實線程的優先級對應,不同操作系統的調度方式也會有許多不同。這最終會造成代碼在不同平台上的行為變得不可預測。這就很難滿足復雜的、大規模並發任務的眾多優先級需求,從而很難達到用戶業務需要達到的效果。
2. 由於在Java系統中,線程被包裝在一個Java語言的對象類—Thread中,所以為了完成Java語言對象和操作系統線程的對應,Java線程的系統開銷還是比較大的(在NT 4.0中,平均每個線程大致占用30KB內存)。因此如果讓Thread對象個數和成千上萬的任務數同比例增長,就顯然是不合理的。
綜上所述,根據並發多任務的大規模需求和Java平台固有的特點,想要利用Java Thread對象的優先級調整CPU資源的分配是非常困難的,所以應該盡量避免讓線程和任務直接對應,也盡量避免使用操作系統線程優先級的調度機制。
解決方案
根據以上分析,問題的症結在於:多任務系統中的任務在Java語言中的對應以及任務間的相互調度。
從本質上看,一個任務就是一系列對象方法的調用序列,與Java的Thread對象或者別的類的對象沒有必然聯系。在避免使用不同操作系統線程調度且同時Java虛擬機又沒有線程調度能力的情況下,要想構造一個協調式多任務系統,讓各個任務相互配合就成了最直接的思路。協調式多任務系統一般有以下特點:
1. 任務由消息驅動,消息的響應代碼完成任務邏輯的處理;
2. 消息隊列完成消息的存儲和管理,從而利用消息處理的次序體現任務優先級的不同;
3. 任務中耗時的消息響應邏輯能夠主動放棄CPU資源,讓別的任務執行(像Windows 3.1中的Yield函數、Visual Basic中的DoEvents語句)。
可能出於巧合,Java語言具有構造協調式多任務系統天然的條件。Java對象的方法不僅是一個函數調用,它還是一個java.lang.reflect.Method類的對象。而所有對象的方法都可以通過Method類的invoke方法調用。如果能使每個任務所對應的一系列方法全部以對象形式包裝成消息,放到消息隊列中,然後再按照自己的優先級算法將隊列中的消息取出,執行其Method對象的invoke調用,那麼一個基本的協調式多任務系統就形成了。其中,任務的優先級和線程的優先級沒有綁定關系。該系統的主體調度函數可以設置成一個“死循環”,按照需要的優先級算法處理消息隊列。對於有多重循環、外設等待等耗時操作的消息響應函數,可以在響應函數內部遞歸調用主體調度函數,這一次調用把原來的“死循環”改成在消息隊列長度減少到一定程度(或者為空)後退出。退出後,函數返回,執行剛才沒有完成的消息響應邏輯,這樣就非常自然地實現了協調式系統中任務主動放棄CPU資源的要求。
如果僅僅做到這一步,完成一個像Windows 3.1中的多任務系統,實際只用了一個線程,沒有利用Java多線程的特點。應該注意到,雖然Java系統中線程調度與平台相關,但是相同優先級的線程之間分時運行的特點基本上是不受特定平台影響的。各個相同優先級的線程共享CPU資源,而線程又被映射成了Java語言中的Thread對象。這些對象就可以被認為是CPU資源的代表。Thread與線程執行代碼主體的接口—Runnable之間是多對一的關系。一個Runnable可以被多個Thread執行。只要將Runnable的執行代碼設置成上述的消息調度函數,並和消息隊列對應上,那麼就可以通過控制為它服務的Thread個數來決定消息隊列執行的快慢,並且在運行時可以動態地新增(new)和退出Thread對象。這樣就能任意調整不同消息隊列在執行時所占用CPU資源的多少。至此,任何一個Java調用都可以在Thread個數不同的消息隊列中選擇,並可以調整這些消息隊列服務的Thread個數,從而實現在運行時調整任務所占用的CPU資源。
縱觀整個方案,由於僅僅基於Java語言固有的Method對象,不同任務間動態分配CPU資源並沒有對任務的性質及其處理流程有任何限制,那麼在消息隊列中沒有高優先級消息時,低優先級消息的處理函數自然會全部占用CPU資源。在不同消息隊列處理速度任意設置時,並沒有將特定的消息限制在快的或者慢的消息隊列上。如果系統的負荷超出(比如消息隊列長度超過一定限制),只要將隊列中低優先級消息換出或者拒絕不能處理的消息進入,那麼系統的運行就可以基本上不受負荷壓力的影響,從而最大保障用戶的關鍵業務需求。
當然,協調式多任務的思想也有其局限性,主要就是它的調度粒度比較大。系統能夠保證的粒度是一次消息處理過程。如果消息處理邏輯非常費時,那麼編程人員就必須再處理函數內部,讓系統主動讓出CPU資源。這雖然需要在處理消息響應邏輯時增加一個考慮因素,但是,在Windows系統盛行的今天,這是一個已經被普遍接受的思路。由於方案中並沒有局限為消息隊列服務的線程數目,所以一個長時間的消息響應只會影響一個線程,而不會對整個系統產生致命的影響。除了調度粒度的問題以外,還有訪問消息隊列操作在各個線程間互斥的問題。取出消息的過程是串行化的,因此對於這一瓶頸的解決方案就是:假設取出一條消息的操作相對於處理消息的消耗可以忽略不計,那麼對於多次調用且僅有兩三行響應邏輯的消息,編程人員通過函數調用就可以直接執行。
前面比較詳細地闡述了多任務系統中任務的劃分以及執行等內容。雖然這些是一個系統的核心,但是在一個實用的系統中,還需要任務間的同步、互斥等機制。在上述框架內,互斥可以簡單地用Java的Synchronized機制實現。由於任務可以主動讓出執行權限,要實現等待(Wait任務中止)和通知(Notify任務繼續),從而實現任務同步也就比較容易了。