這三次做業採用了主線程獲取請求,多級調度器逐級分派,電梯模擬運行的策略。具體來講,主線程實例化ElevatorInput
類,經過阻塞讀取方式得到請求Request
,以後將請求分配給調度器Scheduler
,調度器負責處理請求(既能夠本身處理,也能夠分配給其餘子調度器處理),每個電梯與一個ElevatorScheduler
綁定,用於具體的調度、睡眠控制、打印輸出。java
本次做業的難點主要有如下幾點:python
如何控制調度器、電梯線程的終止:簡單的生產者-消費者模型中,生產者不斷生產,消費者不斷消費,不存在線程終止現象;現實中的電梯,一天24小時運行,沒有異常狀況也不會終止。可是更多的多線程問題是須要考慮線程終止的。這三次做業也是如此:主線程將全部請求都發送給調度器後,告知調度器準備結束,調度器處理完本身隊列中剩餘請求後,結束線程。git
這種通常性的線程生命週期能夠用這個比喻來講明:假如你是一個員工,在上班的時候你可能在幹活,也可能閒着,但即便閒着,也不能回家;到了下班時間後,若是你的任務尚未完成,那就繼續工做(加班),若是完成了,就能夠回家了。因而,咱們能夠抽象出兩個因素:是否空閒(idle
),是否到達下班時間(在電梯的問題中是輸入是否結束,可用inputEnd
變量表示)。是否空閒是能夠本身判斷的,而是否到達下班時間則須要外界的通知。如下討論這種通知輸入結束機制。github
一種比較容易想到的方法是採用interrupt
機制算法
while (true) { Request request = elevatorInput.nextRequest(); if (request == null) { scheduler.interrupt(); } else { // pass } }
可是這種方法並不能實現精確控制:咱們但願的是,若是調度器在等待下一個輸入(wait()
函數中),就打斷;而若是它在執行別的任務,好比sleep(100)
,或者是其餘的同步任務,就不打斷。雖然在這一單元的做業中沒有出現這種狀況,可是多作這種考慮也是合情合理的。編程
另外一種方法是專門設定一個setInputEnd()
方法,由主線程調用,告知調度器輸入結束。安全
class Scheduler { boolean inputEnd = false; Queue<Request> requestQueue; public synchronized void setInputEnd() { this.inputEnd = true; notifyAll(); } public synchronized void addRequest() {} public synchronized void getRequest() { while (requestQueue.isEmpty()) { wait(); } return requestQueue.poll(); } }
我在做業中採用的是這種方法,可是後來發現,其實還能夠用一種更加簡潔的方法解決:創建一個TerminationRequest
類。多線程
interface Request {} class PersonRequest implements Request {} class ElevatorRequest implements Request {} class TerminationRequest implements Request {}
這樣,經過歸一化處理,能夠用一個隊列來統一管理(更通常的來講,是把全部線程關心的狀態改變的通知都放到統一的容器中管理,對外用一樣的接口,對內採起不一樣的處理策略)。同時,這種歸一化也方便了線程安全容器的使用。架構
區分兩種獲取請求的模式:在簡單的生產者-消費者模式中,消費者在沒有商品的狀況下老是會在共享區中的wait
函數中等待,可是在實際生活的不少狀況下,這種狀況是不可接受的——消費者可能還有其餘事情要完成,使用wait
函數等待並釋放CPU資源當然是一種進步,但這種方案同時也制約了消費者進行其餘活動的自由。回到本單元做業,每個電梯在行爲上都須要實現:獲取新的請求,決定電梯的行爲(開門、關門、上行、下行等)。可是這兩種行爲並非時時刻刻都須要進行的:若是電梯局部隊列爲空,電梯內部沒有乘客,則電梯處於空閒狀態,此時不須要頻繁決定電梯的行爲,只須要等待下一個請求的到來。所以,當電梯空閒時,應當阻塞地讀取請求,即在請求處等待下一個請求;而當電梯忙碌時,則只須要查看並更新請求便可,沒有新請求也不阻塞。app
這兩種不一樣的模式,能夠本身解決,如:
class ElevatorScheduler { private Queue<Request> queue; private Queue<Request> localQueue; private synchronized Request getRequest(boolean quickRetreat) { if (quickRetreat) { return queue.poll(); // if the queue is empty, return null } else { while (queue.isEmpty()) { wait(); } return queue.poll(); } } }
也能夠採用Java內置的BlockingQueue
來解決:
private BlockingQueue<Request> queue; private void update() { if (idle) { localQueue.add(queue.take()); } queue.drainTo(localQueue); }
靈活的分配器:第一次做業只有一個電梯;第二次做業有多個電梯,但只有一種型號;第三次做業有不一樣型號的電梯,每一種電梯型號下的電梯數是不一樣的。能夠這樣認爲,第一次做業的電梯只須要一級調度器(直接指揮電梯的調度器),第二次做業的電梯是兩級調度(一級負責電梯見的負載均衡,另外一級負責直接指揮電梯),第三次做業的電梯是三級調度(一級總調度器負責換乘相關管理,一級負責同一個類型的電梯的負載均衡,一級負責直接指揮電梯)。以下圖:
爲了使得分配更加靈活,給這些Scheduler
設計一個統一的接口RequestReceiver
便可,至於內部的處理,或分配或自行指揮電梯,請求提供者都沒必要關心。
interface RequestReceiver { void addRequest (Request r); } class CentralScheduler implements RequestReceiver {} class Scheduler implements RequestReceiver {} class ElevatorScheduler implements RequestReceiver {}
反饋和閉環控制:在實際多線程編程中,反饋和閉環控制也是十分常見的。本單元做業也不例外:換乘須要進行請求的反饋,即電梯運行一部分請求後,由另外一個電梯繼續完成另外一部分請求。既然電梯是逐級控制的,電梯處理完本身應該處理的那一部分請求後,須要將請求反饋給上級調度器,由上級調度器進行二次分配。另外一方面,調度算法在進行調度時,也須要考慮各電梯的負載均衡問題,於是電梯也要上報自身的負載狀況。
這幾回做業中,能夠經過相應線程類提供反饋接口,進行逐級反饋狀態:
interface FeedbackReceiver { void offerFeedback (Feedback fb); void offerRequestFeedback (Collection<PersonRequest> requests); } class CentralScheduler implements RequestReceiver, FeedbackReceiver {} class Scheduler implements RequestReceiver, FeedbackReceiver {}
在反饋反向傳播的時候,每一級Scheduler
也能夠對反饋進行處理,好比做業3中的負載,每一類電梯的負載能夠取這一類全部電梯中負載最小的電梯的負載。
樓層映射:這個問題其實並無什麼面向對象的困難,主要是一個小技巧。每一個電梯調度器(直接指揮電梯進行運動的調度器,它實現了調度算法)有一個映射,實現樓層到樓層下標的快速轉換。
與其經過數學方法實現(分段函數):
int flr_to_ind (int flr) { if (/* some conditions */) { // do something } else if (/* ... */) { // do something } else { // pass } }
不如用Java自帶的方法:
List<Integer> flrs = Arrays.asList(-3, -2, -1, 1, 2, 3); index = flrs.indexOf(flr); flr = flrs.get(index);
假如個人第三次做業真正實現了第一部分中所敘述的思想和方法,那麼再進行擴展也不會很複雜了。但事實上個人第三次做業並無徹底實現這些方法和技巧——程序的主題部分是第五次做業時構建的,以後只作了些小修小補。可是畢竟結構是相似的,也能夠作一些分析:
Request
接口下增長一個緊急制動請求的實現,調度器將這一請求分派到對應電梯。電梯到達下一個停靠點時,經過反饋渠道反饋全部的未完成請求,由上層調度器二次分配,同時電梯線程結束運行。從SOLID角度看:
ElevatorScheduler
類的職責部分重疊,耦合太高。在一開始的設計中,電梯的職能被規定爲負責輸出和睡眠(由於這兩方面相對固定,能夠與易變的ElevatorScheduler
分離,可是在以後的迭代開發中,逐漸職能擴充。RequestReceiver
、FeedBackReceiver
、Runnable
方法,也算是有一點ISP的意思了。main
函數線程只依賴RequestReceiver
接口,也有一點DIP的意思了(雖然沒有實現)。考慮到三次做業結構一脈相承,每次迭代又沒有什麼重大改動,就只分析最後一次做業了。
UML圖:
這裏只實現了二級分派結構,其中PersonTransfer
是課程組提供的PersonRequest
類的子類,表示須要換乘的乘客請求。二級分派結構能夠解決這三次做業的問題,main函數獲取請求,再由高級調度器分派給低級調度器,低級調度器與電梯類協做,實現look電梯調度算法。在算法的實現過程當中,須要管理樓層信息、管理用戶請求信息,這些管理由building類和floor類處理,同時設置FloorNumberManager
類,提供一些靜態方法管理樓層映射、可達性查詢等服務。
這種結構的主要問題是沒有處理好電梯類Elevator
和電梯調度器類ElevatorScheduler
之間的關係。電梯調度器類只擁有一個電梯,負責這個電梯更細緻的調度管理,如每個時間節點,決定電梯上行、下行、開門、關門等動做,主要實現了算法。但同時,電梯類不只負責輸出、睡眠,還負責管理電梯內部人員,檢查到達目的地的乘客,反饋電梯內部乘客信息等。在具體實現中,電梯類又將自身容器暴露給電梯管理類,使得兩個類之間耦合度較高。
此外,電梯類Elevator
並無成爲一個獨立的線程,因此在電梯睡眠時,實際上時是在ElevatorScheduler
線程中睡眠,致使電梯睡眠和電梯調度算法運行沒法並行,下降效率。
複雜度:
能夠看出,調度器是比較複雜的類,而調度器中負責算法的方法又是調度器中比較複雜的方法。可是除了調度器以外,電梯類也比較複雜,這是與設計初衷不符的。緣由在上文也提到過,主要是隨着代碼實現的推動,電梯類的職能不斷擴充,與調度類有所交疊,沒有很好處理這一問題。
協做圖:
這一單元的做業主要容易出bug的地方包括:
我在三次做業的強測和互測中均沒有發現bug,可是個人第一次做業(整個課程的第五次做業)有一個很是嚴重的錯誤(強測和互測,因爲測試機制固定,都沒有檢測出來):若是全部輸入結束後沒有馬上提供輸入結束信號,程序將會進入死鎖,沒法終止。部分代碼以下:
// methods of ElevatorScheduler public synchronized void setInputEnd() { this.inputEnd = true; notifyAll(); } private synchronized void update(boolean quickRetreat) { if ( (inputEnd || quickRetreat) && buffer.isEmpty() ) { return; } while (buffer.isEmpty()) { try { wait(); /* when the thread is notified, it's still in the while loop */ } catch (InterruptedException e) { return; } } // some updates here }
可見,雖然程序退出了wait()
函數,但仍是會再次進入wait()
函數,致使死鎖。一個簡單的修復是在while
循環上增長一個條件;設計上的修復我在第一部分已經提到過,線程檢測到TerminationRequest
後就再也不調用update
函數,把這個問題在線程內部解決。
另外一個問題是沒有處理好CPU資源的讓出,表如今輪詢所致使的CPU_TLE
。通常來講,線程空閒時須要等待,可以使用wait()
函數,一旦線程須要被喚醒,相應鎖的notifyAll()
函數必須被調用。我在第一次電梯做業的互測中用相似如下數據的數據點發現了兩個A屋的solid bug:
[5.0]1-FROM-2-TO-3 [150.0]2-FROM-14-TO-5
本單元測試程序主要考慮本身的使用,包括三部分:
輸入文件的時間映射器,將帶時間的文件輸入映射到時間軸上,實現定時輸入。具體代碼見:https://github.com/YushengZhao/BUAA_OO_elevator_input_mapper
電梯仿真器,模擬電梯的真實運行,在運行過程當中檢查相關問題。主要思路是:將電梯請求和被測程序輸出轉化成若干電梯指令,按照時間排序,在仿真器上模擬運行,在運行過程當中記錄參數、檢查行爲,最終能夠給出性能報告。
請求指令生成器,能夠定製若干段請求序列,每一段能夠設置參數,可參考如下代碼:
def generator(size=10, timescale=10, id_start=1, time_shift=1): # generate one segment of requests pass def generate(): periods = [12,19,8,4,13] sizes = [8,4,10,13,16] s = [] id = 1 time_shift = 0.3 for i, period in enumerate(periods): s += generator(size=sizes[i],timescale=period, id_start=id,time_shift=time_shift) id += (sizes[i]+1) time_shift += (period+0.1)
將這些組件鏈接起來就能夠生成測評機了,可是考慮到自身實際需求,就沒有具體實現了。
一個值得注意的地方:許多同窗採用python腳本進行時間映射,我在參考了以前一些學長的博客以後發現,這種方法容易產生時間誤差,時間控制不是很精確,而將時間映射器內嵌到Java語言內部則能夠實現更精確的控制。同時,這樣也便於調試。
關於請求生成策略:實際應用中可能會出現不一樣時段負載不一樣的狀況,我在測評機中按段生成請求,能夠模擬這種狀況。在進行幾回到幾十次測試後(總請求量1e2
量級),通常沒有什麼顯著問題;進行大量測試(1e3,1e4
量級的測試)也許能夠發現一些問題,但考慮到每個測試所消耗的時間成本,就沒有過多測試了。真正實際應用中,大量的測試確定是必要的。
這一單元的調試和測試與上一單元相比,主要是多了時間因素,在測試時要考慮輸入隨時間分佈的不一樣特徵,如在一個時間點大量輸入,在一段時間內沒有輸入,等等。而在調試時,因爲不能使用斷點調試法,我廣泛採用了日誌記錄的方法,增長一個可插拔的logger:
private static final boolean LOGGER = true; private static final boolean ASSERTION = true; public static void log(String msg) { if (LOGGER) { System.out.println(msg); } } public static void ast(boolean condition, String msg) { // ast == assert if (ASSERTION && !condition) { System.out.println("Assertion failed: "+msg); } }
或者實現一個帶level
的不一樣重要性的日誌輸出:
private static final int LEVEL = 5; public static void log(int level, String msg) { if (level >= LEVEL) { System.out.println(msg); } }
固然,Java也有相應自帶的日誌記錄機制,不過考慮到這一單元做業並不須要複雜的日誌,就沒有使用了。
多線程方面,我在作電梯第一次做業時花了比較長的時間(甚至一度覺得本身就要止步於此了),當時有許多問題想不清楚,最後實現的代碼也有許多邏輯混亂的地方。多線程之因此容易出現各類安全問題,歸根結底仍是線程自身行爲邏輯複雜,好比,簡單的生產者-消費者模型,基本不會有人寫出線程安全的問題,可是複雜一些的生產者-消費者模型(如消費者在沒有產品時不調用wait
函數,而是進行其餘活動),就容易產生線程安全問題了。所以,根據需求創建簡單通用的線程模型很是重要——簡單的邏輯每每不容易出錯。好比觀察者模式,構建了一個線程發佈消息,若干線程接收消息的模型;反過來,又有時也須要若干線程發送消息,某一個線程接收消息的狀況,這時即可以採用消息隊列:
class Scheduler { private Queue<Message> messageQueue; public synchronized void addMsg() {/*some code here*/} public synchronized Message getMsg() {/*some code here*/} } interface Message {} class Feedback implements Message {} interface Request extends Message {}
全部須要通知Scheduler
的消息,都經過addMsg
方法傳入,不管其具體內容,以後再由其餘函數分別處理。所以,當Scheduler
處理完剩餘任務後,即可以直接在getMsg
方法內等待。
設計原則方面,我在完成第一次電梯做業時,並無太多的設計原則方面的意識,而因爲後兩次做業都沿用第一次做業的結構,全部後兩次做業也沒有體現多少設計原則,這是遺憾的。
代碼重構方面,我從這一單元開始堅決了能不重構就不重構的態度。從計組到如今,在各類須要迭代開發的工程中,我老是感受以前寫的不夠好,想要重構,但又常常忽視了重構的風險以及不重構的可能性。實際開發中,重構確定是要儘可能避免的,在原來的代碼(多是看似比較亂的代碼)上修改其實才是常態。事實上,看似亂糟糟的代碼其實可能並無想象中那麼差。我在第二次電梯做業時曾經嘗試了重構,可是等到真正動手寫重構代碼的時候才發現,若是重構,不少代碼都是差很少的,原來不少設計上的考慮都是頗有道理的。
算法方面,這一單元的做業給算法留出了很大的空間。不過想拿高分並不須要很複雜的算法。最基本的look算法,在強測不出現錯誤的狀況下,基本就能夠達到95分以上了。後兩次做業再加一些負載均衡的考慮,若是沒有錯誤,基本也能拿到95分以上。儘管如此,討論一些算法也是沒有壞處的:
固然這些算法都是我沒有實現的,畢竟課程的主要目的也不是算法。
其餘:
Building
,給每個Building
安排若干Floor
,並提供「向上的按鈕」和「向下的按鈕」接口,就像現實生活中的電梯同樣。電梯不知道一個樓層有多少人,只知道某一樓層有沒有人想上樓、有沒有人想下樓。