oo5_7html
多線程電梯時,我還執着於時間的精準性,也就是上下樓必定要多少多少秒,因此採起的是假時間策略。java
爲了實現假時間策略,我將三部電梯的運行封閉到了一個線程當中,單獨一個線程內部的執行是不會受到線程調度產生的偏差的影響的。git
在這基礎上,考慮到輸入IO會有阻塞,安排了一個輸入線程。至於調度器線程,對於使用假時間策略的個人設計而言,它是無關緊要的,畢竟實際的調度都是在電梯線程內部進行的,若是不在電梯線程內部進行,那麼調度與執行之間就會產生線程調度致使的時間偏差,會致使正確性問題。故而個人調度器線程雖然按照指導書要求而加上了,但它僅進行部分不會產生時間偏差的調度功能。github
在輸入線程與調度器線程之間僅有指令須要傳遞,我才用了一個阻塞隊列來實現指令隊列。利用java庫中自帶的同步容器類保證線程安全。web
在調度器線程與電梯線程之間,存在兩類同步問題:canvas
第一類同步問題,由於我採用的是電梯線程內部假時間的策略,因此很容易解決,只須要利用一把調度器線程與電梯線程共享的「運行鎖」便可。電梯線程每次主循環開頭獲取鎖,主循環末尾釋放鎖。當調度器線程但願中止電梯線程時,只需獲取該鎖便可。同時爲了不飢餓問題,這裏我採用了公平鎖,在只有兩個線程爭奪該鎖的狀況下,性能損失仍是能夠接受的。緩存
第二類同步問題,由於當調度器線程訪問電梯狀態時,電梯線程一定中止,不可能更新自身狀態,故而僅需確保電梯狀態都能反映到內存中,而不是被緩存。故而我大量使用了volatile變量、原子變量實現輕量級的同步。安全
線程劃分很是明細、簡單:ruby
首先考慮觸發器線程組,它們之間共享的是被監控的文件,而這份線程安全性被委託給了FileCenter這一線程安全的File類的封裝類。服務器
再考慮summary線程、record線程,它們之間沒有共享,但各自都和許多觸發器線程共享了它們記錄的信息。這是典型的「讀者-寫者」的狀況。寫者是一堆觸發器線程,讀者是須要將記錄的信息寫進文件的summary、record線程。我採用了消息隊列的方法保證了寫者與讀者的同步,將線程安全性委託給了java的同步容器類。
這一次我拋棄了多線程電梯時注重正確性的策略,沒有采用假時間策略。故而這裏100個出租車再也不只有一個線程,而是真正的100個線程。
同時,我注意到對乘車請求的響應、「搶單窗口」的設計很是適合使用服務器模型進行實現。故而我安排了一個線程池,這個線程池中一個線程對應於正在處理的一個乘車請求,稱該線程爲調度單元線程。
除此以外,就還有一個標配的輸入線程。
首先,出租車之間共享地圖,以及調度單元。
其次須要保證出租車的狀態對其餘線程均可見,這一點經過簡單的內部鎖便可實現。
最後由於須要經過位置、狀態來訪問出租車,故而我安排了一個緩衝用的TaxisMonitor,出租車監控類來存儲緩存信息。由於該緩衝對象會被全部出租車訪問來更新緩存,故而須要進行同步。這裏我採起了細粒度加鎖策略,畢竟自己就是爲了性能而作的緩衝,不能由於加鎖反而損失性能。對於每個位置上一個鎖,每一種狀態上一把鎖。
不過由於每一次出租車狀態更新會須要訪問先後兩個狀態,若是同時獲取兩個狀態的鎖,會致使死鎖問題。個人解決方法是讓程序同一時間要麼只獲取前一狀態的鎖,要麼只獲取後一狀態的鎖,雖然會致使出租車在一段時間內在緩衝區中不可見,但能夠簡單解決了死鎖問題。而不可見致使的正確性損失,在此次做業中並沒有傷大雅。若是真的會由於這點時間的不可見而產生正確性錯誤,那出租車線程自己運行的時候就會由於過卡而致使走一條邊超過200s了。
此次電梯做業光看度量的面板數值還能夠,也就輸入處理那裏我圖省事嵌套多了點。可是實際上由於是屢次更迭的項目,其中有衆多冗餘的代碼。這點從55個類、2898行代碼中就能夠看出。
從面板數值上能夠看出,此次IFTTT做業最大的問題就是,它的分支判斷至關地龐大。這一點我實在想不出怎麼避免,我已經將分支判斷儘量封裝在一個方法中,而且保證該方法的接口統一性。或許能夠採用將分支判斷數據化,而後編寫自動進行分支判斷的代碼來解決。
狀態變化——這是此次出租車的紅點。我在思考可否經過將狀態自己也給抽象出來,做爲一個類對待?而後狀態變化的邏輯交由狀態自己來處理?或許這樣就能將複雜的圈邏輯降維,分散到各個狀態類中去。
從LiftsThread以右下的那一部分代碼所有都是和單線程時一致的,也就是電梯內部依然是按照單線程時的運做模式進行運做,再也不贅述其內部實現。
爲了可以複用單線程時的代碼,我在LiftsThread內部,將系統時間的流逝轉化成了對Lift響應模擬時間變化的調用次數。也就是每過幾幾秒就調用一次Lift一個時間粒度的變化函數。
因此實際上,LiftsThread僅僅只是一個用來封裝模擬時間的線程,其內部不包含任何調度邏輯。
實際的調度邏輯所有被包含在Schedular及SubSchedular中。其中SubSchedualr爲單部電梯時的調用邏輯。Schedular爲協調三部電梯的運動量均衡策略邏輯。
SchedularThread僅僅是爲了迎合指導書要求而贅寫的中介線程。
InputThread負責讀入指令,並將其放入CommandTray中。以後SchedularThread從CommandTray中取出指令,再暫停LiftsThread,轉交給它指令。
World負責系統內時間的管理。
ifttt中我大量使用了繼承,主要緣由是指導書的不明確以及來自助教的需求的頻繁變動,致使了代碼須要不斷維護。爲了減小代碼維護時的工做量,我儘量地複用代碼,減小同質代碼的出現。
繼承樹一共有四支。
FileCenter即爲一個線程安全的File類的封裝類,負責文件讀寫的底層封裝。
出租車的類設計主要分爲了三個族:
Taxi族。這部分包含了Taxi, Driver, TaxisMonitor。這一族內部高度耦合,三位一體地實現了出租車。
Timepasser爲Taxi的基類,封裝了Taxi的線程邏輯。負責將系統時間的流逝轉化成Taxi內部時間的每時間粒度的流逝。
Schedule族。這部分包含了ScheduleServer, ScheduleUnit。ScheduleServer維護了一個線程池,該線程池中每個線程都爲ScheduleUnit的實例化。這一族負責響應乘車請求,並對每一個乘車請求分配一個線程進行搶單、分單等邏輯。
除了這三族之外,還有幾個類是爲了以後的擴展而額外實現的:
關於這個,我難以進行分析……由於我不知道我有沒有作到。究竟作到什麼程度才能算是作到了某一項原則?我沒有足夠的經驗去回答這個問題……我只能說我儘可能去作了。
主要的bug來自於對指導書理解有誤以及未及時看issue和微信羣。其次的bug大可能是由於我沒寫正則去檢查輸入是否合法,只要輸入錯了,那我就挑正確的而後繼續跑,跑不下去再跑錯,可是公測要求無論能不能繼續跑都要報錯。
我沒怎麼查別人的bug,沒那個時間,僅僅只是按照公測要求完成了課程安排的工做。
其實多線程的同步控制並不困難,與電梯複雜的調度邏輯相比,那是很簡單的東西。代碼編寫過程當中最大的難點實際上是兩點:
我很想說我知道這個問題的答案,但我作不到。僅僅只是閱讀指導書,進行需求分析,並不能真正導向「正確的代碼」,而只是能接近。在知道這件事情之後,我所能作的或許僅僅只是留出「將代碼改正確的餘地」。
在程序寫完前我無從得知程序的性能如何——這很顯然,可是我卻必需要寫完它。若是我在編寫過程當中就考慮性能問題,那頗有可能就會致使我代碼根本寫不完,或者越寫越錯。由於優化性能的代碼邏輯每每是複雜的。在第一次編寫的過程當中我每每會採起最簡單的那種方法,而不是最快的方法。我所能作的或許僅僅只是留出「將代碼變快的餘地」。
上述兩個問題最終都導向一點:餘地。也就是代碼的可修改空間、可拓展空間。當我編寫的程序不僅是寫完就行,過了OJ就行的時候,「餘地」成了我編寫代碼時須要重點考慮的因素。
在電梯的時候,我十分固執於程序的正確性,經過不斷地發issue明確指導書的意思,我雖然不能徹底作到,可是卻能向「正確的程序」接近。
不過在ifttt時,需求頻繁的變動、指導書的不明確讓我放棄了對正確性的固執——那是一件作不到的事情了,一樣一份代碼,兩我的看,一我的以爲對,另外一個以爲錯,再也不有統一規定。
公測逐漸轉爲互測,互測時bug樹能夠用一個bug掛滿,許多人的公測由於對方沒測而滿分,吐槽版各類認領代碼,等等現象,讓我明白了一件事情:這課的成績沒有很大的意義,就和個人博雅課程的成績的意義差很少。雖然這課的成績會算進GPA,可是這課的設計就不是以給學生成績爲核心的。
言盡於此,並非抱怨,這就是我所見到的,我所想的。