面向對象第二單元訓練總結

1、前言

第二單元的三次做業是頗有特色的三次做業。多線程電梯的設計思路和前兩次電梯做業迥然不一樣,致使我花費了大量的時間去重構以前的代碼,使其適應多線程電梯的做業要求;文件監視器是一個獨立的做業,不像電梯和出租車那樣是一個系列,所以寫起來沒什麼包袱,感受並不困難;出租車調度和多線程電梯寫起來感受比較類似,但出租車幾乎沒有算法上的難度,所以主要的工做都花費在瞭如何構建一個好的設計上面。這三次做業之間看起來沒有什麼關聯,但卻環環相扣,一步一步加深着我對多線程編程的理解。算法

我對這三次做業的整體難度評價爲:多線程電梯 > 出租車調度 >= 文件監視器。(這個難度基本上是根據個人熬夜時間來判斷的) 編程

之因此排出這樣的難度順序,是由於多線程電梯和出租車調度有着一個共同的難點,而這個難點是文件監視器所不具有的——程序的運行時間須要與這個世界的真實時間保持同步。這是這兩次做業的一個大坑,也是我在好幾個深夜裏不睡覺而被迫面對着電腦屏幕的罪惡源泉。雖然多線程極大地加強了用戶與程序交互的即時性,可是爲了同時保證交互的即時性和邏輯正確性,編程者須要付出許多額外的努力和工做。數組

2、多線程電梯

電梯系列做業是讓我寫得很不爽的三次做業。第一次的傻瓜調度,我設計了一套我自認爲十分精妙的判斷同質的算法,從而幾乎沒有阻力地無傷經過了公測和互測;但到了第二次ALS調度,噩夢就開始了:我發現本身的傻瓜調度算法徹底沒法移植到ALS上面,於是不得已更換了算法,並大面積重構了程序;到了多線程電梯,我又一次痛苦地發現,以前的ALS調度算法與多線程電梯的即時輸入是不相容的,只好被迫又一次地重構。三次做業,三套算法,三種設計,若是有一我的連續三次分配到這樣的代碼,恐怕他根本不會認爲這三次做業都出自同一人之手。早知如此,我在第一次電梯做業就應該使用模擬爬樓的算法,這樣就不會有後面這麼多糟心事兒了。安全

拋開這些悲傷的過往不談,個人多線程電梯採用了與老師總結課上PPT類似的設計:當一條請求輸入進來以後,會被髮送到一個總請求隊列中。主調度器根據當前三部電梯的情況,把這條請求派發到合適的電梯中去。每個電梯保有一個本身的小請求隊列和小調度器,主調度器派發的請求進入某一部電梯的小請求隊列以後,會由這部電梯的小調度器來判斷是否須要進行捎帶。這樣設計的好處是,把判斷同質的過程和判斷捎帶的過程分離,將一個大的調度器類拆成兩個調度器類,從而減小調度器類的代碼量。數據結構

此次做業遇到的一個難題是:怎樣讓電梯精確地知足"運行一層樓花費3.0s,開關門一次花費6.0s"。由於是第一次接觸多線程編程,對sleep和wait的用法還不太熟悉,爲了保證公測可以經過,我採用了模擬時間的方法,即輸出的是所謂的"電梯系統時間",是假的、事先計算出來的,而非直接取自系統時間。在電梯運行的過程當中,讓電梯線程sleep三秒鐘或者六秒鐘,以使模擬時間和真實時間同步。固然,既然使用了這種方式,就勢必面臨着時間差的問題。我解決這個問題的方式是:在電梯線程的無限循環裏面,每一次循環體開始的時候先獲取一下當前的系統時間,到循環體的最後判斷一下已通過去了多少毫秒,並從睡眠的時間中把這個數字減掉。經過使用這種方式,個人程序運行得還算精準,總體偏差不會大到一個不可接受的程度,互測中很難被發現與此相關的Bug。多線程

本次做業的經典OO度量狀況以下:架構

可見多線程電梯的實際代碼量並很少,只有1000行左右(這其中還包含了實質上並無被用到的ALS調度器和傻瓜調度器)。可是因爲第一次使用多線程編程,對run方法和臨界區域還不太熟悉,致使在電梯線程裏的代碼嵌套層數過多,如上圖中紅字所示。併發

本次做業的類圖以下:性能

從雷圖中能夠看出來,本次做業在設計上存在着過分封裝的問題。爲了知足同步控制的要求,我在電梯類以外建立了一個Elevators類,其中用數組將三個Elevator類的實例包含在裏面,調度器只能與Elevators類進行交互,而不能直接訪問某一部電梯。這樣作看似合理,但其實是徹底沒有必要的。過分的封裝使代碼變得醜陋和臃腫不堪,須要無數個getter和setter才能完成所有所需的操做,這毫無疑問對代碼質量是有害的。此外,因爲懼怕線程安全問題,我對Elevators類中的幾乎全部方法都使用了synchronized標識,這樣作雖然加強了程序的線程安全性,卻極大地損害了併發性,同時至關程度上下降了性能。這些都是在以後的做業中須要改進的地方。學習

本次做業的時序圖以下:

此次做業的線程協做設計較爲合理,主調度器將請求派發至各個電梯保有的小請求隊列,並在內部進行捎帶判斷,這極大地減輕了主調度器的工做量。

3、文件監視器

文件監視器做業是我認爲本身寫的比較順利的一次,各類功能都很完備,也沒有被別人挑出什麼Bug。我想一方面緣由是,此次做業的指導書規定不夠明確,Readme的做用被無限放大,致使任何事情只要在Readme裏提一句,就可讓對方沒法扣本身分。例如,設計者甚至能夠強制要求測試者在兩次文件操做之間加入間隔,這使得程序的算法難度幾乎降爲0,甚至失去意義。再者,指導書明確規定,兩次文件掃描操做間隔內不容許對同一個文件實施兩次或以上的修改,這也很大程度上讓此次做業變得很水。

文件監視器的主要訓練目標是讓同窗們可以作出一個線程安全的設計,但並無強調對於性能的要求,這是我認爲此次做業一個很大的不足。若是沒有性能要求,設計者徹底能夠把全部的方法都加上同一個鎖,這樣就能夠保證不會出現資源爭奪的現象。可是這樣作對學習是沒有幫助的,甚至是有害的,我以爲在下一屆的課程中,應該對文件監視器的性能有着更高的要求。

致使此次做業難度不大的另外一方面緣由是,文件監視器並無時間上的要求,即程序的時間不須要與外部真實時間保持同步。所以,設計者能夠採用各類手段使本身的程序知足指導書中規定的要求,即便這些手段是以性能的損失爲代價的。整體來說,文件監視器是一個很獨特的做業,既不承上也不啓下,大概能夠算做是兩次系列做業(電梯和出租車)之間的一個小插曲。

本次做業的經典OO度量以下:

從經典OO度量中能夠看出,本次做業的代碼規模控制得很好,只有752行,且各類方法調用的嵌套深度都保持在一個合適的範圍內。圖中的紅色警告是main方法,這是由於我將記錄Detail和Summary的線程以匿名內部類的方式直接寫在了main方法裏,因此致使塊調用深度大於均值。

本次做業的類圖以下:

文件監視器的設計難度並不大,各個模塊之間的層次也比較清晰。我設置了一個Snapshot類不斷捕獲文件結構快照,並在其內部對新舊兩次快照進行對比,從而判斷是否有文件發生了變更。在數據結構方面,我選擇了HashMap而非樹形結構,由於對於這次做業的要求(不須要比對文件夾,只須要比對監控區下的全部文件)來說,樹形結構的性能並非很好,遠遠比不上HashMap的效率。

本次做業的時序圖以下:

可見程序總體的邏輯並不複雜,無非就是在一個無限循環中不斷捕獲快照並進行對比。

4、出租車調度

相比於文件監視器,出租車調度要難寫得多。這個難寫不在於其算法,而在於出租車的要求多且雜。最使人痛苦的一個要求是一輛車移動一格的時間必須嚴格保證爲200ms,這幾乎就直接限制了程序的時間方式,即必須採用模擬時間,而後讓程序的sleep時間向模擬時間靠攏。爲了解決這個問題,我採用了sleepUntil方法,即先計算出租車應該在何時到達,而後再讓程序睡到那個時間。這樣作雖然有一點點耍賴,但確實很好地完成了指導書中的要求。

此次做業是系列做業,所以須要一開始就打好一個設計的基礎。但很惋惜的是,我並無完成這個任務,由於在此次做業快要截止的時候,我發現本身的程序沒法很好地處理同時有不少個請求一塊兒輸入的狀況。這個問題也在互測中被測個人大佬一下就挑了出來。究其緣由,是由於我爲每個請求都開啓了一個線程,並讓其運行三秒鐘後自行終止,這雖然很是符合真實的邏輯,但卻不適用於程序自己。由於每個請求線程均可能會改變出租車的狀態,所以須要爲這個請求線程中涉及到變動出租車狀態的地方加鎖,一旦請求變多,達到百條的量級,就會使得線程之間互相阻塞,後面的請求得不到執行。此外,因爲用戶能夠自由輸入請求,因此實際上程序的線程數是由用戶控制的,這顯然是一種極不安全也極不合理的設計。在進行下一次出租車做業以前,我會想辦法解決這個問題,把線程數控制在一個本身可控的範圍內。

本次做業的經典OO度量以下:

此次做業的代碼量並不大,1473行是包含了GUI的統計,將GUI排除在外後,實際只有900行不到。但我仍然以爲程序在許多地方顯得過於臃腫,請求隊列類幾乎形同虛設,出租車線程設計得也不夠優雅。這些須要在重構的時候加以解決。

本次做業的類圖以下:

在類設計中,幾乎全部的數據操做都是圍繞TaxiSet類展開的。TaxiSet包含了全部出租車的信息,請求線程只能訪問到TaxiSet類,而不能直接對Taxi進行操做。這使得多個請求線程可使用synchronized以保證不會出現數據衝突的狀況。

本次做業的時序圖以下:

TaxiDispatcher出租車派遣類是整個程序執行流程的核心。TaxiDispatcher就是我所說的只會運行3秒鐘的線程,它會從請求隊列中提取請求,並通知乘客出發點周圍的出租車搶單,並最終決定調度哪一輛出租車爲乘客服務。

5、Bug分析

個人程序在多線程電梯和出租車調度中各被報告了一個Bug,其中多線程電梯是因爲忘記對某一塊輸入部分進行處理而致使的公測格式錯誤,出租車調度則是上文中提到的沒法同時處理大量請求的錯誤。前者是因爲粗心馬虎和測試不周全而致使的Bug,後者則純粹是由設計致使。值得注意的是出租車調度的Bug,它使我對程序內線程數量和程序性能的關係有了更深的理解。

多線程電梯的互測中,我找到別人的Bug主要集中在捎帶的判斷上。多是因爲模擬時間和真實時間的同步沒有作好,有些應該判斷爲捎帶的地方對方並無判斷成功。我想這種問題很難從代碼層面直接挑出來,只有經過大量樣例的測試才能發現。文件監視器的互測中,我主要經過閱讀別人的代碼發現了Bug。對方沒有作好重命名時的多映射檢測,也沒有完成指導書中要求的繼續監控移動後文件的任務,這些Bug均可以在仔細閱讀代碼之後直接找到。更深層次的緣由是我在寫程序的時候也遇到了這些問題,所以在互測的時候就會對它們格外關注。出租車調度的互測中,因爲代碼量較大,且直接從代碼中找邏輯Bug相對困難,我採用了集中壓力測試的方法,即一開始就讓全部的出租車集中在地圖的左上角,而後集中輸入請求進行壓力測試。經過這樣的方法,一些隱蔽的Bug才能被發現。

多線程程序的代碼邏輯相比單線程程序複雜不少,有時候直接閱讀代碼也難以找到其中的漏洞。這個時候,測試樣例的廣度覆蓋和壓力測試的深度覆蓋就顯得頗有必要了。此外,找到別人Bug的另外一個好方法是回顧本身的設計過程,細數本身在寫代碼的時候踩過哪些坑,而後再去看別人是否犯了相同的錯誤。

6、心得與體會

不少同窗都將多線程稱之爲"玄學",我想這是有必定道理的。不一樣於單線程程序的徹底可控,多線程程序在運行的過程當中可能會出現許多難以預料的行爲,甚至有些行爲不可復現,但對程序卻有着致命的影響。編程者該作的,不該該是想着如何迴避甚至掩蓋這些問題,而是應該努力地去暴露問題,並爭取對其加以修復。

提升多線程程序的性能並不困難,保證多線程程序的線程安全也不困難,但要想同時作好這兩點,就變得很是困難。在這三次的做業中,我遇到的幾乎全部多線程問題均可以歸根結底爲一句話:如何在性能和安全之間作出取捨。程序的時間須要和真實時間保持一致,這是對性能的要求,然而爲了兼顧多線程的安全性,編程者可能須要採起一些同步控制的方法,這其中的時間差勢必會致使程序時間和真實時間的不一樣步。這三次做業中,我嘗試了一些解決這個問題的方法,最終發現,將模擬時間和真實時間結合起來,先計算出程序應該運行的時間,而後再讓它睡到那個時間,這種方式既省腦子,也省資源,還能確保程序運行的正確性。

除此以外,程序的架構須要有一個足夠優秀的設計才能經得起需求變動的考驗。在寫代碼以前,先花一兩天的時間在紙上寫寫畫畫,大體勾勒出程序的框圖;而後先不寫方法的主體定義,只寫方法名和返回值,用這些尚待完善的半成品方法和類的屬性搭出一個程序;最後,爲每一個方法填上具體的內容,完成整個程序。這一套流程能夠有效地檢驗程序設計是否合理,也必定程度上減輕了工做量。我在出租車調度做業中使用了這個方法,並取得了令我滿意的成果。

多線程編程仍是頗有意思的,當看到出租車在GUI上動起來的那一刻,個人心中真的有一種巨大的成就感。但願接下來的三次做業也能像前兩個單元同樣順利。

相關文章
相關標籤/搜索