學會與「有生命力」的對象打交道——面向對象設計與構造第二章總結

面向對象第二章博客總結-多線程電梯

OO的多線程電梯做業已經結束了,回顧三次做業,我對多線程設計、編程和調試有了初步的認識與見解,在任務不斷加深的過程當中感覺到任務結構所存在的優點和問題。藉助此次的博客,如今從如下幾個方面進行總結:java

  • 三次任務的電梯設計策略
  • 度量性分析
  • 分析程序的Bug
  • 互測策略
  • 心得體會

1.多線程電梯設計策略

第一次做業

初次接觸多線程編程,我採用最基礎的生產者-消費者模型來實現,共享對象爲RequestList,生產者線程(主線程)負責接受請求並向請求隊列投遞請求,而消費者線程(電梯)則採用對RequestList的逐個取出指令並執行。算法

第二次做業

在第二次做業的結構中,採用了輸入(主)線程-調度線程-電梯線程的三線程結構,結構已有意識地爲多電梯運行和任務分拆創建結構,具體來講,有如下特色:編程

  1. 輸入線程與調度器線程之間的通信採用「托盤」結構:然後的做業涉及任務的分拆與電梯的反饋,經過「托盤」結構,可使任務來源對調度器是透明的,調度器將對子任務和新任務視做一樣的任務,從而簡化結構。
  2. 調度器線程與電梯線程之間通信採用「觀察者」模式:便於多電梯的註冊。
  3. TaskList:列表中以運行任務爲單元進行優先隊列的維護,實現具備必定優化的ALS調度策略。

第三次做業

第三次做業需求的主要變化在於多任務調度、不一樣的電梯運行屬性和運載任務的分拆,爲此第三次的結構在第二次結構拓展而來,並主要作了以下的改進:安全

  1. 類的抽象和工廠模式:引入了電梯工廠根據關鍵詞建立不一樣類型的A、B、C電梯,適應不一樣的速度、容量和停靠樓層。
  2. 電梯反饋與請求池:第三次做業中因爲停靠樓層的不一樣,於是任務須要進行分拆,對於須要分拆的任務,結構中有如下的類和線程執行對應功能:調度器——將新任務取出並生成可執行的子任務,電梯線程——反饋子請求完成狀況,請求池——接受電梯的反饋,基於乘客當前樓層與目標樓層以決定是否生成新任務
  3. 基於樓層的任務列表:雖然Elevator和TaskList在結構上與第二次做業保持相同,但TaskList實質內容已發生較大的改變,經過在每一個樓層創建接放乘客的列表(相似於樓層類),以實現Look的掃描算法。


2.OO做業度量分析

對於這三次做業,我從第二次做業纔開始認證考慮並實現架構,第一次做業基於最基本的生產者-消費者模式,類的定義和功能不多且相對固定,於是在此僅展現類圖,而將具體的OO度量放在做業二和做業三共用的架構上。數據結構

做業一類圖

做業二和三的度量分析

做業二和做業三所用的架構是一致的,做業三相較做業二,對於每部電梯除了其停靠樓層、速度和容量作了微調,其主要差別集中體如今主調度器中調度算法實質地填充(在做業二中主調度器是一個形式化的空殼)。多線程

類圖

方法和類的複雜度分析

經過觀察上述表格,複雜度太高的方法集中於兩個關鍵詞:調度和掃描;複雜度太高的類集中於電梯和任務列表的過耦合架構

  1. 調度:對於調度器,因爲本次做業中電梯的相關屬性已經很是詳細地給出了,所以個人調度策略實際上是固定套路,天然會有許多的if-else if-else的條件判斷,於是分支複雜度就很高。不過,在徹底基於先驗知識的狀況下,我認爲固定策略是一種較優的選擇,在這一點上方法複雜度太高不可避免。
  2. 掃描:掃描模塊主要功能是判斷電梯是否須要沿方向繼續行駛並對方向進行改變,但因爲個人接放乘客乘客列表徹底獨立於電梯(也就說沒有用單獨列表維護在電梯中的人),結構很是獨立和鬆散,所以當要統計和改變運動趨勢時運行開銷會很大,解決方法也就如上文所述使用列表維護電梯中的乘客
  3. 電梯和任務列表的耦合:主要緣由是在劃分功能時,把電梯和其對應的任務列表進行了錯誤的劃分,電梯物理運行+乘客行爲這樣的劃分方式在拓展功能時遇到了很大的阻力,由於電梯和乘客之間的相關性隨着功能增多而愈來愈強,解決方法應該考慮電梯爲主而任務列表爲輔的架構,將主要功能都側重於電梯實現。

ev(G):Essentail Complexity,用來表示一個方法的結構化程度,範圍在\([1,v(G)]\)之間。併發

iv(G):Design Complexity,用來表示一個方法和他所調用的其餘方法的緊密程度,範圍在\([1,v(G)]\)之間。異步

v(G):循環複雜度,能夠理解爲窮盡程序流程每一條路徑所須要的試驗次數。函數

OCavg:表明類中方法的循環複雜度的平均值,具體來講是因爲條件分支和嵌套複雜狀況

WMC:表明類的總循環複雜度,具體來講是方法之間互相調用複雜狀況

依賴性分析

![](https://img2018.cnblogs.com/blog/1616500/201904/1616500-20190424213136437-111964170.png)

根據上述數據,除去一些tool靜態工具類,本次做業各種之間的依賴性比較正常,於是側面反映出對各模塊功能的劃分仍是比較正確的(固然elevatortaskList兩個類之間耦合是個意外,不過二者做爲總體對外還不錯)

協做圖(第一部分已給出)

時序圖

SOLID分析

  1. SRP-單一責任原則:從宏觀上看,各線程各司其職,功能劃分比較合理;從微觀上看,每部電梯與其對應的任務鏈表責任劃分不清晰,致使後期優化時實現複雜度和運行復雜度都比較高。

  2. OCP-開放封閉原則:有意識但作得不夠標準不夠好,在第二次做業時,爲第三次做業的相關功能預留了空白的函數,但並無這些具備拓展潛力的部分抽象爲接口和類(如調度算法、電梯自有屬性等)以實現嚴格的開閉原則。
  3. LSP-里氏替換原則:未涉及類的繼承。
  4. DIP-依賴倒置原則:符合,特別是對於RequestPoolElevator,兩個模塊在新功能加入後基本沒有改動。
  5. ISP-接口分離原則:符合,主要是由於在程序中實現的接口類方法並很少,只有主調度方法和樓層方向的工具類。電梯的運行算法採用了Look,因爲維護數據結構的特色,幾乎徹底內嵌很差變動運行策略。


3.分析程序的Bug

在本章的做業中,個人程序在公測和互測階段均沒有被發現Bug,我認爲這主要取決對架構的重視、課下大量的自動測試和保守的synchronized。於是在本章中,我將着重討論本身在結構設計時如何避免產生bug的一些想法:

  • 線程安全:線程不安全都源自於對線程間共享數據的處理不當,於是其實解決這樣的問題其實能夠從儘量少的共享數據+共享數據操做徹底安全兩個方面來考慮,要作到這樣,個人程序實現如下方面:
    1. 線程間的功能解耦:程序結構中三類線程的功能高度封裝,絕大多數的工做都是創建在私有數據之上,這使得共享數據的數量很是少。
    2. 線程通信的兩個惟一:惟一的共享對象,惟一的通信接受接口,對於Dispatcher,其數據的輸入接口有且僅有RequestPool的get類方法,對於Elevator,其數據的輸入接口僅有TaskList的方法,這樣使基於notifywait的守護者模式變得很安全(由於線程的守護對象是惟一的)。
  • 樓層數據的標準化:任務中,負的樓層、樓層方向的設置與判斷、樓層合法性的判斷等狀況若是直接地引入對電梯的數據處理是不利的,爲此,這些數據在電梯中應當被標準化(在電梯內部以自定義的語言理解,而內外溝通則使用可逆的轉換),我在程序中定義了FloorTool靜態方法類集中解決這些問題:
public static int index2Floor(int index);
    public static int setDirectionDown();
    public static int setDirectionUp();
    public static int setDirectionStill();
    public static boolean isDown(int dir);
    public static boolean isUp(int dir);
    public static boolean isStill(int dir);
    public static String getDirectionName(int dir);
    public static boolean isLegalFloorIndex(int floorIndex, int[] legalList);
    public static boolean isDirectTransport(int from, int to, int[] legalList);
    public static int getDirection(int from, int to);
  • 打印Log信息:個人Log信息主要由線程對共享數據的讀寫、線程狀態的變化兩部分構成,當出現線程安全問題後,利用Log信息將更加方便地找出問題。
@<Elevator A>:State -> Rest
@<Elevator C>:State -> Rest
@<Elevator B>:State -> Rest
<Dispatcher>:Get a New Request '1-FROM-3-TO-1'
<Dispatcher>:Task <1-FROM-3-TO-1> dispatched to C
@<Elevator C>:State -> Recover
<Elevator C>:A new Task '1-FROM-3-TO-1' Have Been Validated
<Elevator C>:Direction Change: STILL -> UP
<Elevator C>:Direction Change: UP -> STILL
<Elevator C>:Direction Change: STILL -> DOWN
<Dispatcher>:ID 1 Task Finished
<Elevator C>:Direction Change: DOWN -> STILL
@<Elevator C>:State -> Rest
@<Client>:State -> Input Terminated
<Elevator A>:Have Received Input Terminate Signal.
<Elevator B>:Have Received Input Terminate Signal.
@<Elevator A>:State -> Recover
<Elevator C>:Have Received Input Terminate Signal.
@<Elevator A>:State -> Normal ShutDown With 0 Tasks
@<Dispatcher>:State -> Normal ShutDown
@<Elevator B>:State -> Recover
@<Elevator C>:State -> Recover
@<Elevator B>:State -> Normal ShutDown With 0 Tasks
@<Elevator C>:State -> Normal ShutDown With 1 Tasks
@<Main>:State -> Normal ShutDown

4.互測策略

  1. 互測結果總結
    1. 本章的測試中,我採用的是自動化爲主+手動爲輔的測試策略,自動測試改動自何岱嵐(再次膜)同窗開源的評測系統,實現數據生成+數據投放與接收+正確性判斷全套功能,而手動輔助測試屬於定點轟炸,在第二章中關注ALS調度算法的漏洞,在第三章中關注主調度器策略和任務終止時的處理。
    2. 最經過手動測試幫助我發現了同窗的Bug:這個Bug存在於第二次做業中,這位同窗的ALS調度算法在電梯接乘客方向與乘客運動方向相反時會直接失效,徹底變爲傻瓜調度,進而調度嚴重超出了規定時間。
  2. 線程安全測試策略
    1. 時間臨界點的投放輸入:以0.0秒、三部電梯徹底休眠時、接近終止時間時等臨界點投放。
    2. 高併發輸入:在自動化測評時將隨機時間按固定梯度劃分,實現批量指令同時投放。
  3. 測試策略與第一章內容的差別
    1. 手動評測:輸入數據爲定時投放,設計的測試數據須要藉助Python、Java、Bash等腳本手段進行投放。
    2. 自動評測:受助於優秀的大佬同窗開放自動評測的關鍵技術,在第二章做業中我實現自動評測的過程當中並無很大的阻礙。除了手動評測時實現定時投放外,另外一個重點則是正確性判斷(在第一章可以使用sympy,但本章中須要手動實現指導書中的判斷邏輯)。

5.心得與體會

異步線程間的通信

本章做業中,模塊劃分好了,各個線程專人專事,在功能上就沒什麼大問題,但難點在於各個線程之間協同運行,我所遇到的最大問題就是異步線程之間的通信和同步問題,爲解決這個問題,我基本仍是使用了多線程編程中的生產-消費者及守護者模式,僅使用wait()notifyAll()兩種線程狀態控制,這種模式及其思想大體出如今了程序架構的以下幾個方面:

  1. <HM5,輸入線程-電梯>,<HM6, 輸入線程-調度器>:基礎的托盤式結構。
  2. <HM6, 調度器-電梯>,<HM7,調度器-電梯>:托盤型結構的變形,調度器經過觀察者模式主動向電梯發送調度請求,請求被放置於每一個電梯獨有的「Cache」中,在電梯while循環中會專門調用方法去清空Cache並生效任務。
  3. <HM7,輸入線程,電梯-調度器>:增長電梯對調度任務的反饋,電梯本身既是間接消費者,也是直接生產者。

我認爲在這種模式驅動下的線程其特色最重要的就是隱私性和主動性,線程之間因爲存在」托盤式「的設計,線程內部的隱私性都很高,共享對象不多,須要考慮的線程安全問題就要少不少;同時,藉助守護者模式,消費者線程主動地按需獲取和主動地進入結束狀態,這令外部線程對本線程運行狀態的影響和干預縮小到了局部而固定的代碼上,減小未知狀況的討論,固然,這也對線程自身運行的邏輯完備性提出了更高要求,不然若是出現問題其餘進程也一籌莫展。

在邏輯完備性的考慮上,我認爲最須要注意的即是多條件下的守護者模式對終止條件的判斷,進過屢次的嘗試,我逐漸地摸索出了在編碼實現的模板:

public synchronized getRequest(){
    while(requestList.isEmpty()){                       // 守護條件
        if(inputTerminate && runningTask==0){           // 守護條件下的特例
            return null;
        }
        wait();
    }
    return requestList.remove(0);
}
  1. 首先,一個線程通常只能守護一個共享對象:若是出現對多個共享對象的守護,很容易出現顧此失彼和死鎖的狀況,就像HM7中調度器須要在電梯完成子任務後獲得反饋並安排新的任務同樣,電梯反饋的內容要麼被傳回至RequetPool和輸入線程傳入的請求同等對待,要麼子任務的下達由電梯線程運行調度器的方法完成。
  2. 其次,守護條件及其特例的書寫要有規範:經過本章做業中,一種守護條件+特例的組合方式就是上述的模板,首先明確當要守護時,必要不充分條件是請求列表爲空,在進入了while循環後,經過剔除特別狀況使得執行wait()時是守護的充要條件。固然,還有一種書寫思路,那就是直接將守護條件寫成充要條件,並在後續的操做中對特例進行判斷。
  3. 最後,阻塞線程的喚醒必定要考慮wait語句前的全部條件分支:while()循環條件的變量固然算一個,可是在進入while後條件判斷分支也必須考慮,在模板中:requestList,inputTerminate,runningTask三者對wait的執行都有影響,所以任何對while的條件判斷及其內部語句中的成員產生變化的,都須要加上對應的notifyAll()語句。

程序設計的不足

  1. 對其餘的線程通信方式嘗試較少:

    上文已經提到,基於生產-消費模式的信息交互模式對線程自身邏輯的封閉性和完備性要求很高,但隨着多線程編程後續功能和狀況逐漸複雜,經過主動獲取托盤信息的方式可能顯得不可行。這時候可能就須要外部線程直接使用interrupt()等手段讓程序陷入異常態進行處理,所以還須要對更多的線程協調和通信方式進行了解。

  2. 鎖優化:

    本章做業並無涉及到實際業務狀況中高併發的狀況,於是並無促使我過多的考慮鎖優化,幾乎全部的共享對象都採用的是synchronized()的方法。

  3. 電梯和其一對一對應的列表耦合重:

    耦合性太高的問題隨着優化方法的嘗試和優秀同窗架構的分享而逐漸顯現,總結其緣由,主要仍是在於對電梯功能劃分時策略不夠好:原設計中將電梯自己和乘客的行爲分割並分別用ElevatorTaskList實現,但實際上後續當須要結合乘客分佈和電梯狀態進行預測時,二者因爲平級,於是耦合很大。

    如今考慮,仍是應該將TaskList做爲輔助,以Elevator爲主進行運行和乘客的運動。

  4. 任務列表TaskList結構過鬆散:

    因爲實現Look算法,我實現了基於每一層樓的PickListPutList以表示電梯須要在此層接放的乘客,這種方式確實能很好地實現Look算法,可是若是在改進時涉及到仿真預測、乘客選擇性拿放時,這種鬆散的數據結構就須要不少的輔助數據來維護,也就是方便於一次性寫入但繁瑣於後續修改

    目前的初步改進想法是,保留PickList但去除PutList,乘客的投放交由電梯內部一個小隊列處理。

相關文章
相關標籤/搜索