在我開始寫此次博客做業的時候,窗外響起了但願之花,由此聯想到乘坐本身寫的電梯FROM-3-TO--1下樓洗澡,而後······算法
開個玩笑,這麼辣雞的電梯確定不會投入實際使用的,況且只是一次做業。仍是從中認真分析一下經驗和不足吧。設計模式
如今看起來,做業一的難度在整個單元真的僅僅至關於熱身。使用線程安全的集合類ConcurrentLinkedQueue存儲線程,再用while(true)輪詢的方法實現電梯的獲取請求,得到請求後運行電梯,運行結束後等待下一個請求便可。整個程序甚至不須要使用wait(),notifyAll()和synchronized修飾符。數組
惟一的一個坑點在於終止條件的設定:除了輸入終止外,還須要當前存儲請求的隊列爲空。有一個條件不知足,電梯就不能中止運行,對應在程序中也就是Elevator的線程不能中止(不要停下來啊?:)安全
第一次做業的類圖和度量分析以下。性能優化
從數據表現上看,因爲本次做業比較簡單,設計複雜度並不大,但個別地方仍有提高空間。多線程
另外,在Elevator對象中又跑了一個具備run方法的Queue對象(換句話說,輸入和隊列是一個合而爲一的類),在第二次做業前的OO理論課上提到了這種狀況應該避免,因而後兩次做業Input和Queue(Monitor)都是分開的。框架
在實際寫代碼以前,我也想過了一些或大或小的調度策略上的優化,但在簡單程度和效率上均無明顯優點。我也想過諸如是否某層開門即上所有人(題目要求是上前進方向相同的人)之類的問題,但仔細考慮以後,也確實未必比題目要求的作法快,因而仍是忠實按參考算法實現了第二次做業的電梯策略。性能
按照以前說過的拆開Input和Monitor以後,整個程序更加接近於典型的生產者——消費者設計模式,生產者:Input線程;消費者:Elevator線程;緩衝區:Monitor。爲優化效率和減少設計難度,我在Monitor的設計上下了工夫,用HashMap代替Queue,以fromFloor、direction(也就是signum(toFloor - fromFloor))做爲key,每一個key對應一個Queue做爲value,存儲特定樓層出發、前往特定方向的請求集合,而後根據題目要求的算法,改變電梯運行的流程,實現可捎帶調度。學習
第二次做業的類圖和度量分析以下。測試
這一次Elevator類和Monitor類的設計明顯複雜許多,主要體如今run()方法和mainRequest的設置上。
在設計時,因爲電梯的運行狀態受mainRequest的運行狀態直接影響,所以要考慮mainRequest的各類狀況,尤爲是direction這一參數,在上行、下行與暫停時都要仔細處理。
即使如此,在強測後仍存在一個bug,未能想到辦法解決。
這一次做業,據老師所說多是本學期難度的頂峯,在艱難的coding過程當中,我可謂深有體會。
從電梯的初始化開始,此次做業就充滿陷阱。
每一個電梯有不一樣的速度、載客量、可達樓層,在初始化時必須將它們做爲參數導入進去。別忘了,還有電梯的ID。
而因爲電梯在同一棟樓裏,最低樓層和最高樓層是相同且有限的。很天然地,我想到用boolean數組表示電梯在各樓層容許停靠的狀態。
電梯的初始化就算解決了。
而對於Monitor,因爲存在一些靠單個電梯不能直達的請求,爲了實現這些請求的拆分處理,Monitor必須知道每一個電梯可停靠樓層,所以用於初始化各個電梯的可達樓層數組信息,必須所有導入Monitor。
至於Input,這時候拆分Input和Monitor的好處體現出來了——能夠直接沿用上一次做業的Input類。
若是不是由於請求拆分,此次做業的難度其實也不會特別大。
但就是由於有了這個拆分請求的特殊要求,此次做業就有了一個超級大的坑點。對於須要換乘的請求,因爲存在A->B->C的時序關係,不能簡單的拆分爲A->B和B->C。怎麼辦呢?只能自建一種結構來存儲這些須要換乘的請求。先存儲這個請求本來的樣子,再用一個Queue(固然用List也是能夠的)來按時序存儲這些請求拆分後的分請求。至於拆分點的選擇,就根據以前初始化Monitor時的數組,選擇一個從fromFloor和toFloor均可以直達的樓層(這裏有一個限制,由於本次做業每一個請求最多換乘一次,因此每一個請求最多也就兩開花,不用再作更復雜的細分。這確定是限制了這個程序的可擴展性,可是生活實踐中也不多出現須要換兩次以上電梯才能到達目標樓層的狀況吧)。至於在哪一個樓層進行換乘,我採用了比較簡單的作法:理論最近距離。也就是在全部可以換乘的floor中,選擇abs(fromFloor-floor)+abs(toFloor-floor)最小的floor。這個作法的靈感是去年DSP課上的地鐵最短距離,應該說更貼近於用戶的思惟:怎麼短怎麼來,距離短的走法確定快。其實論實現難度,還有更加簡單的拆分方式,是我室友採用的:1層和15層每一個電梯都能到,選這兩個樓層就完事了。
對於每一個Elevator的調度,能夠採用第二次做業的捎帶算法,不過是加了些限制條件:只能拉取本身可到達樓層的請求、容量已滿時不能上人等。
以前提到Queue裏按時序存儲拆分後請求,這樣對於每一個Elevator,拉取請求時都是從Queue中拉取第一個請求,就避免了出現時序錯誤(先B->C後A->B)的問題。
一樣是爲了簡單起見,各個線程各自從Monitor中獲取(更準確的說叫作「搶」)知足條件的請求,獲取以後就不存在由別的電梯來執行這一請求的可能性了(怎麼有種下一單元出租車的感受?),避免執行時可能出現的混亂。
原本覺得這樣就結束了,結果······3箇中測點沒過?
屢次debug後終於發現問題所在:存在一種狀況,Monitor的Queue空,輸入已經中止,但電梯中運行着須要換乘的請求,這個請求跑完後,又要放回requestList中,而按照以前的處理方式,程序已經視爲結束了,能換乘的電梯確定都停了。距離結束還剩兩個小時,怎麼辦?
辦法總比困難多!在這生死存亡的危急時刻,我想到了一個簡單的方法:再建一個Queue專門存儲須要換乘的請求,當這些請求的拆分出來的第一個分請求執行完之後就從Queue中刪除,這樣終止條件加上一條:這個新建的Queue也空。問題就解決了。
但是一提交,前9組全過,最後一組CPU時間超時,不知道由於什麼,有電梯進程開始了魔鬼般的瘋狂輪詢。怎麼辦?只能採用沒辦法的辦法,遇到輪詢先sleep必定的秒數,雖然治標不治本,可是CPU時間確實降低了足足90%,過掉了最後一個點。
意外又發生了,跑了同窗的強測數據,炸了幾乎全部的點。仔細一看,出現問題的指令都是通往特定樓層的。稍微想一想都知道,是那個面向過程時代遺留下的bug類型:數組處理錯了。畢竟樓層有正有負,中間卻少了個0層,映射到從0開始的連續的數組下標,狀況仍是有點複雜的。改掉以後,測試點全過,強測和互測都沒出現bug。
第三次做業的類圖和度量分析以下。
毫無疑問,程序又複雜了一大截,不過相比上一次做業,類圖結構沒有大的變化,仍然是典型的生產者——消費者設計模式。
最後強測中的效率並不理想,幾乎都是保底的分數。除了電梯的調度算法外,我想到了一些能夠在細節上優化的地方:
a.在換乘樓層的選擇上結合電梯運行狀態,而不是機械地選擇最短路程。
b.換乘請求執行時,執行第二級請求的電梯預先到達換乘樓層,提升效率。
c.電梯滿員且不下人時不停靠。
······
若是實現這些細節,也許性能分能提高一大塊。但寫的時候時間已經嚴重不足(由於搞OS,週一纔開始寫此次OO的做業,截止前2小時才de完bug),也只能說是遺憾吧。
什麼,互測?這個應該說在本身寫電梯的時候投入了太多精力,無意再hack其餘人的bug了吧。另外一方面,即使想hack其餘人,也沒有找到一個可以定時投放需求的方法(也就是不會寫測試程序)。
多線程一時爽,線程調度火葬場(確信)
在第一次做業中,能夠說是很好地體會到了多線程的好處,然後兩次做業,則是完徹底全地體會到了寫多線程程序的辛苦,不管是wait()和notifyAll()的正確運用、synchronized修飾符和鎖的正確使用,都讓我花費了好多時間才搞清楚。更別說複雜的線程調度了。
也正是在這種複雜中,我更加深入地體會到了依照固定的設計模式進行程序設計的重要性。它使我從賊費腦細胞的程序框架設計中解脫出來,直接進入具體問題的解決環節,用上一單元指導書的話講,真的就是避免了「重複造輪子」,能夠說是受益不淺了。在接下來的OO學習中,熟悉幾個經典而用處普遍的設計模式,用模板化的思惟來節省設計時間,把更多的時間投入在性能優化上。