電梯模擬系統——BUAA OO第二單元做業總結

需求分析

官方需求

  • 本次做業須要模擬一個多線程實時多電梯系統,從標準輸入中輸入請求信息,程序進行接收和處理,模擬電梯運行,將必要的運行信息經過輸出接口進行輸出。
  • 本次做業電梯系統具備的功能爲:上下行,開關門。本次多部電梯的可停靠樓層,運行時間,最大載客量都不相同。
  • 電梯系統能夠採用任意的調度策略,即上行仍是下行,是否在某層開關門,均可自定義,只要保證在系統限制時間內將全部的乘客送至目的地便可。
  • 電梯系統在某一層開關門時間內能夠上下乘客,開關門的邊界時間均可以上下乘客。

簡要分析

此次做業有幾個關鍵點:實時系統,正確調度,多線程交互。這就要求在設計中須要提早定義好運行邏輯,並且經歷了第一單元的歷練,在第一次做業就須要對後續做業可能出現的狀況進行設計——即儘可能最大化程序的可拓展性,下降程序模塊之間的耦合程度,這樣在需求變化時就能儘可能少的修改代碼達到需求。<br>本次做業三個階段性安排爲:java

  • 第一階段: 多線程單部電梯,先來先服務原則運行(即電梯一次執行一個任務),樓層號連續
  • 第二階段: 多線程單部電梯,可捎帶策略運行(即電梯中容許攜帶多人,無最大人數限制),包括地上(正數)和地下樓層(負數)【要求使用wait/notify機制,不能使用進程輪詢,下同】
  • 第三階段: 多線程多部電梯,可捎帶策略運行,電梯有最大人數限制,電梯可停靠樓層、運行時間有不一樣限制

本單元做業輸入輸出接口均已給出,所以重點就在於程序進程交互邏輯以及任務分配執行的算法。python

邏輯設計

由於有了第一單元設計的經驗,爲了減小每次的修改量,在每次做業的實現中都儘可能採用邏輯分離式設計,每一個模塊獨立的從隊列中取出信息、處理信息、放置信息(反饋)。模塊之間只經過隊列交互,而後獨立的實現一套邏輯。針對各個部分,設計變化以下:git

  • 第一階段: 直接按照推薦的模式將需求和任務進行了分離,即線程交互分爲兩個過程:
    • 第一過程使用單獨的線程處理輸入,並建立請求隊列,由Submission和Scheduler交互訪問——Submission讀入輸入放置請求,Scheduler取出請求佈置任務。
    • 第二過程由Scheduler與Elevator進行交互,共享Mission隊列與ElevatorState電梯狀態板——Scheduler取出請求後讀取狀態板,分配任務給電梯(由於只有一部,因此這個階段並無具體實現分配算法函數,只是返回當前電梯名);電梯根據任務隊列執行任務,運行時主動更新狀態板信息。
    • 爲了保持多電梯的擴展性,我在Scheduler中將可調度電梯設置成了「註冊」模式,即主線程建立電梯對象後,調用addElevator(Elevator ele)函數添加一部電梯使得電梯可被調度。
  • 第二階段: 第二階段除將輪詢交互改成wait/notify機制以外,主要增長的需求爲捎帶和負數樓層且-1->1的樓層變化並不連續。
    • 針對樓層需求,我設置了有序列表使得樓層之間的變化保持連續性:github

      List list = Arrays.asList(-3,-2,-1,
          1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20);

      這樣在程序中修改樓層因此便可實現樓層之間的連續變化。算法

    • 針對捎帶需求,在個人設計中將可捎帶斷定與捎帶執行分配到了電梯本身的線程中,所以這部分基本只對Elevator類進行了修改,在電梯開門、關門、變化樓層、任務開始時遍歷Mission隊列決定可捎帶的任務並判斷下一個停靠的目標樓層,以適應不斷到來的任務變化。設計模式

    • 須要注意的是,此次由於捎帶的發生,須要進行Mission狀態的記錄——即人在電梯內或者電梯外->進一步爲了簡化邏輯,將這個狀態的判斷轉化爲「Mission在執行隊列或等待隊列中」。這樣作的好處是能夠將上下電梯統一塊兒來,基於上下電梯須要的電梯行爲表現(到達-開門-關門)是統一的這點,可使得電梯邏輯變得簡單,至於上或下只須要體如今輸出便可。安全

  • 第三階段: 第三階段實現了真正的多電梯交互,並且提升了樓層不連續性的限制,並對電梯人數進行了限制,這些相應的限制實際上是對調度器提出了需求;除此以外,換乘狀況的增長我直接採用了任務分解處理。同上,這個單元依舊不須要更高Submission模塊以及其與Scheduler的交互,甚至Scheduler與Elevator的交互邏輯也不須要更改,只須要更改各自的處理算法便可。
    • Scheduler中須要修改的就是以前預留的String determine(Mission request)函數
      /**
      * 考慮:電梯運行狀態+電梯選項+目標電梯
      *      輸入Mission:目標換乘樓層(須要換乘時)
      *      1. 若是選擇只有一個,直接上
      *      2. 若是選擇有多個,基於「換乘點是一致的」的條件
      *         分支樹斷定(方向、捎帶、遠近、人多少)
      * @param request
      * @return 目標電梯名
      */
      private String determine(Mission request)
      另外,由於不一樣的電梯線程都須要訪問調度器的部分方法,將Scheduler做爲了Main類中的靜態實例處理,不知道算不算單例的一種實現,但由於Scheduler中保證了線程安全,並且只在主線程中實例化,所以這個靜態實例變量能夠正確使用
    • ElevatorElevatorState兩部分配合修改實現了樓層可達性控制‘’‘’‘’‘及Mission分階段執行。在原來樓層的list的基礎上在構造方法中增長了
      this.availableFloor = new ArrayList<>();
      this.availableFloor.addAll(floors);
      來斷定可停靠的樓層,至於具體是哪些,在建立電梯時指定,電梯中還增長了相應的接口進行樓層判斷。實現整套邏輯。
    • Mission部分爲了利用現有的邏輯完成換乘設計,將請求按照換乘須要拆分紅了不一樣的Mission階段,request與Mission扔保持一一對應關係來保證乘客不會出現「分身」的狀況,執行完一個階段由電梯將此Mission交還給Scheduler從新參與調度。所以,在電梯狀態板增長了「是否可達的」斷定:
      public Boolean canDeliver(int floor) {
          if (!this.availableFloor.contains(floor)) {
              return false;
          }
          return true;
      }
      相應的在Scheduler中提供了函數判斷是否可直達
    • 電梯任務分配算法可表示爲:
      /**
      * 可直達? |--yes--> 使用直達電梯
      *         |--no---> 拆分紅兩階段
      *
      * 當前階段性任務有多部電梯可完成:1.當前方向上可捎帶? ---> 2.電梯中人數較少?
      */
      這樣就可以較爲合理地規劃電梯的任務分配並處理好換乘狀況。

最終獲得的總體邏輯的時序圖以下:多線程

時序圖

BUG分析

本次做業第一階段和第三階段都未發現bug,第二階段強測中發現了一個bug,主要是由於第二階段對調度性能要求較高,可捎帶的狀況必需要捎帶上才行,而我在設計Mission隊列以及Elevator正在執行的Mission存儲兩部分同步不是很好,致使Mission隊列中有任務,卻由於檢查不及時Elevator沒有檢查並執行這個任務,尤爲是某一層有不少人上電梯時(同時有不少人同一樓層的用戶請求到來)。這個bug的出現和輸入的時機有關,所以本身測試的時候沒有檢測到。<br> 第三階段我花費了很長時間在debug上,由於評測機一隻返回RUNNTIME_ERROR,但個人程序並不會出錯,爲此,我還實現了簡單地隨機測試腳本:架構

import random
pair = []
l = [-3, -2, -1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
for i in l:
	for j in l:
		if i == j:
			continue
		else:
			pair.append((i,j))
n = 0
time = 0.0
with open("data.txt", 'a') as f:
	for t in random.sample(pair, 50):
	    print("[{:.1f}]{}-FROM-{}-TO-{}".format(time, n, t[0], t[1]))
	    status = f.write("[{:.1f}]{}-FROM-{}-TO-{}\n".format(time, n, t[0], t[1]))
	    n += 1
	    if random.random() > 0.6:
	    	time += random.random()
	f.write("\n")

以上使用python自動生成數據,藉助Mistariano提供的黑箱接口能夠進行簡單地測試,數據生成能夠經過調節參數控制app

@echo off
set num=0
:start
set /a num+=1
echo ================No. %num% time======================= >> err.txt
python gen.py | "C:\Program Files\Java\jdk-11.0.2\bin\java.exe" -cp out\production\oo_course_e_2019_16071064_homework_6;..\..\elevator-test-suit-0-3.jar Main >> err.txt 2>&1
echo ================    over      ======================= >> err.txt
goto start

雖然使用了大量的黑箱測試個人代碼卻依然有RUNTIME_ERROR的問題,因而我又嘗試其餘的debug,終於發現是由於程序退出時Scheduler發生了輪詢致使CPU時間超時(這一點評測機並無反饋,並且錯誤種類不該該是這個啊),這裏,JProfile的線程分析幫了我大忙

JProfile分析結果

這個界面顯示了每一個線程實際運行的時間,若是有線程使用輪詢致使CPU時間過程能夠明顯的看出並直接定位。

代碼分析

類圖

相比較以前的設計,這個單元我認爲做爲比較好的是在第一次代碼就肯定了整個的設計框架,也證實在新需求到來時能夠較快的適應改動。

類圖

從圖中能夠看出各個類的功能劃分是比較清晰地,關於共享對象的設計以及線程類的實現是和預期設計一致的。並且每個線程類都不依靠其餘線程,是一個獨立的邏輯體,這大幅下降了模塊之間的耦合性,也使得代碼邏輯更爲清晰易懂。

度量分析

  • 代碼量分析

    代碼量分析

    此次的代碼相比較第一單元有了很大的改觀,首先是類的代碼量,大部分功能類的代碼量分佈仍是比較均衡的,代碼量最多的類源碼沒有超過300行,並且在完成過程當中我也嘗試使用Javadoc風格的函數與類功能註釋,註釋比例有了很大的增長。重要的是,使用這樣的註釋以後在下一個階段須要更新代碼的時候可以很快的回憶起函數的功能與實現。<br>此次類間代碼量的均衡得益於功能上的分工明確,即將不一樣的功能實現交由不一樣的模塊完成,但其中由於電梯的運行策略最爲複雜,代碼行數較多。但總體上仍是比較合理的。

  • 方法複雜度分析 此次代碼的複雜度分析呈現出如下結果,共出現了三個複雜的方法,一個複雜的條件判斷,一個長參數列表問題,這些問題的應該是能夠解決的。總體來看,代碼的循環複雜性(CC)爲2.49122807,其中循環複雜性最高的函數依舊是iterMission,高達13。這個函數的功能是遍歷Mission隊列並添加Mission到電梯的執行隊列中,從這一點來看,這個函數仍是有能夠簡化的空間的。

Class Name Method Name Code Smell
Elevator Elevator Long Parameter List
Elevator checkDirection Complex Method
Elevator iterMission Complex Method
Scheduler run Complex Method
Scheduler determine Complex Conditional
  • 類複雜度分析 總體來看,類之間仍是比較均衡的,和最初的設計一致,只有Main和Submission較爲簡單,這兩個類自己也沒有什麼複雜的邏輯也不須要完成什麼實質性的算法功能,所以仍是比較合理的。
Type Name NOF NOPF NOM NOPM LOC WMC
域數量 public域數量 方法數量 public方法數量 行數 類加權方法數
Elevator 8 0 15 4 302 58
Main 1 0 3 3 37 4
Mission 6 0 10 9 118 17
Scheduler 5 0 11 7 200 33
Submission 2 0 2 2 46 4
ElevatorState 9 0 16 16 121 26

總結與感悟

這是我第一次接觸多線程程序,以前只是在學習操做系統的時候瞭解過原理,但並無動手寫過多線程程序。經過此次實踐我不只掌握了多線程程序的設計方法,並且瞭解到設計模式在多線程交互中的關鍵性。不管是線程安全仍是死鎖預防,或者是線程之間wait/notify的等待喚醒機制,都讓我對線程交互有了更深層的理解,並且認識到使用JProfile分析程序運行狀態進行優化的必要性。<br>真正測試代碼時,白盒測試是比較直接的方式,但黑盒測試優點也能提供意想不到的效果與便捷性,並且從某種程度上講更能達到壓力測試的效果。<br>另外,在設計之初就考慮可擴展性是頗有必要的,一套好的架構必定是可以支持不斷擴展的架構,進行好架構設計對功能擴展不管是工做量仍是安全性都有很好的幫助。

相關文章
相關標籤/搜索