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