OO第二次博客做業

OO第二次博客做業

寫在前面

  通過了三週多線程做業的的洗禮,平常在猝死邊緣試探的我能有幸活到今天,先在此慶祝一下。雖然過程十分痛苦,但這三次多線程編程做業仍是讓我受益良多。其中有一些矛盾給我留下了深入的印象,我把這些矛盾總結爲如下3點:java

  1. 多線程的隨機性與結果的準確性的矛盾
  2. 多線程的併發性與線程安全性的矛盾
  3. 線程的數量與程序性能的矛盾

這些矛盾的具體表現我將在下面的做業分析過程當中進行詳細的描述。算法

多線程電梯

1.設計策略

  第一次多線程做業正值清明假期,時間充足,我花了兩天時間作了大量的前期準備工做,包括學習java多線程編程的有關知識以及做業的前期規劃,我將前兩次寫的臃腫而醜陋的電梯進行了重構,保留原來的核心調度策略(雙隊列:請求隊列,等待隊列;主請求;按鈕),幾乎是重寫了所有代碼,雖然違背了做業要求繼承上一次的意圖。可是我以爲此次重構是值得的,也是很有成效的。
  此次做業使用了經典的生產者消費者模型,使用阻塞隊列來保證共享數據的安全性,相通的部分我就再也不贅述,如下是我設計的調度器調度策略,電梯運行策略以及調度器和電梯之間的協同關係。
編程

2.程序結構分析




  程序主要由請求發生器,調度器,電梯三個類構成,三個類之間經過Request對象,阻塞隊列和按鈕開關來傳遞信息。
  請求發生器負責根據控制檯輸入產生請求,判斷請求格式的合法性,並將請求加入與調度器共享的阻塞隊列。
  調度器負責分配請求,其內有兩個隊列,一個是與請求發生器共享的請求隊列,另外一個是等待隊列。具體調度策略如前面的流程圖所述。
  電梯負責運行,根據當前的主指令和捎帶隊列中的指令定時調整自身的狀態。
  因爲調度器須要獲取電梯的狀態進行判斷,爲了不出現線程安全問題,有的狀態的get方法用synchronized作了同步,有的狀態直接使用原子類和原子操做。
  此次做業的結構上我還比較滿意,因爲把原來調度器的部分功能移到了電梯內部,電梯類顯得有一些龐大,電梯類和調度器之間共享數據較多,使用了大量同步來避免線程安全問題。設計模式

3.bug分析

  此次做業因爲準備極爲充分,前期規劃作的也很紮實,本身調試過程當中幾乎沒有遇到功能性bug,公測和互測也都沒有出現bug。可是有一個矛盾十分明顯,就是前面所述的第一條多線程的隨機性與結果準確性的矛盾,因爲請求的輸入時間的不肯定性,形成電梯的運行實際上有至關程度的隨機性,然而測試又須要準確的結果,這二者之間的矛盾幾乎達到了不可調節的程度。主要衝突有兩點:安全

  1. 使用假時間仍是系統時間
  2. 時間邊界的處理

  因爲程序運行自己是須要時間的,若是僅僅經過輸出系統時間會致使程序運行時間一長就會產生累計偏差,因此不少同窗意識到這個問題以後都轉向了假時間陣營(假時間就是在輸出信息時使用的是一個經過起始時間通過一系列過程計算獲得的一個絕對準確的時間),後來又衍生出了用理論須要睡眠的時間減去實際運行時間獲得實際睡眠時間的「真假時間」陣營。
  時間邊界問題在前期是個人一個心頭大患,舉一個例子,若是有三臺電梯理論上同時到達2層,但運動量各不相同,在到達以前有一條10層的樓層請求還未處理,按調度原則應該分配給運動量最小的電梯,但實際上這三臺電梯並非真的同時到達,總會有幾毫秒的偏差,這就會致使這條請求被分配給了第一個到達2層的電梯,而不是運動量最小的電梯。我在這個問題上的解決方案是:電梯到達某一樓層後向調度器發出分配請求信號,調度器收到信號先睡眠20ms再開始分配,這20ms能夠確保三臺電梯都已經到達了2層,從而解決了這個問題。固然這算是一個比較糟糕的解決方案,可是當時因爲經驗不足,即便是這樣一個糟糕的解決方案也是與同窗們進行大量探討後得出的少有的可行方案。在通過了出租車做業後我又有了一種新的解決方案——設置最小時間粒度,這個方法能夠很好地解決時間邊界的問題,這個方案將在出租車做業中進行詳細介紹。
  在互測階段我拿到的那份代碼,在運動量的處理上問題較爲嚴重,公測就出現了問題,可是基本功能一切正常,只不過代碼的結構與命名規範作的並很差,有UML協做圖面條式代碼的嫌疑,單調度器就有400行左右,可讀性極差,我也就放棄了讀代碼的過程直接結合readme測試。通過一番狂轟濫炸,基本全部錯誤都與運動量相關,而這個點公測就已經掛了,因而只好上殺手鐗,就是上一段所述的那個多臺電梯都處於運行狀態,多臺電梯同時中止,中止時刻的請求分配問題。果不其然,分配給了第一臺到達的電梯,出現了bug。
  因爲此次做業是第一次接觸多線程,尚未講線程安全的有關問題,也沒有考慮如何測試線程安全相關問題,純粹使用控制檯本身把控時間進行輸入,如今回想起來就以爲當時真的太原始了,寫一個測試類多簡單。多線程

4.心得體會

  通過此次做業,我對多線程編程有了一個初步的認識,對生產者消費者模型也有了必定的瞭解。我我的認爲此次做業的代碼也從開學到當時寫的最好的一次,不管是從每一個類的職責,各個類之間的協同關係,每一個方法的職責,以及總的代碼量來看作的都比較出色。併發

IFTTT

1.設計策略

  此次做業是目前爲止我認爲最坑的一次做業,爲何要用「坑」,主要緣由是若是論難度此次做業實際上與上一次做業沒有明顯的提高,可是此次的指導書簡直就是天書,裏面有太多的問題沒有解釋清楚。這直接致使我到週日的晚上尚未搞清楚此次做業到底要寫個啥。無奈之下我只好結合指導書以及issue和客服羣裏各類五花八門甚至是自相矛盾的回答,運用「奧卡姆剃刀」(如無必要,勿增實體,即簡單有效原則),作了一個總結,對各類觸發器的觸發條件以及對目錄的監控以指導書爲依託,把解釋不通似是而非的部分所有「剃掉」,極大地簡化了做業的難度,提出了對指導書的簡化版理解。讓我欣慰的是個人理解獲得了助教的贊同,也得到了衆多同窗的支持,以致於我發佈的這個issue#32出如今了不少同窗的readme中,我在互測階段拿到的這份做業就是這樣。
  此次做業開啓了一個新的思路,就是狀態快照。經過快照記錄下某一瞬間的監控目錄下全部文件的狀態信息,按期拍攝快照並將兩個相鄰快照進行比對發現變化,因爲快照中的文件信息的各個域都是在快照生成的瞬間就已經肯定下來不會改變的,因此在監視器訪問快照的時候也就不存在線程安全問題。
  因爲此次做業實際上從週一開始才正式動工,時間很是緊迫,幾乎沒有前期規劃,算法寫的也不好,每一個路徑上的一個觸發器對應一個線程,每一個觸發器的頂層目錄擁有一份快照,全部快照經過一個HashTable進行管理,key值爲目錄文件自身或是普通文件的上層目錄,減小了一部分重複的快照,可是仍是有至關多的冗餘。全部觸發器每隔1s喚醒一次,喚醒後保存以前快照而後刷新快照,將新的快照與以前保存的快照做對比,找不一樣,看是否知足觸發條件,若是知足執行對應任務,爲了防止多個觸發器同時執行任務致使一些莫明奇妙的事情發生 (recover任務最爲明顯),不得已對全部觸發器線程作了互斥,另外一方面爲了不文件類出現安全性問題對全部文件操做設置了靜態鎖,從而保證了同一時刻只能有一個線程在對文件進行操做,從而實現線程安全,這就扯出了前面說到的第二條矛盾:多線程的併發性與線程安全性的矛盾,個人這種處理方式明顯是以犧牲併發性爲代價的,因爲將觸發器類的run方法中除了sleep的部分所有都加了靜態鎖,實際上文件狀態管理部分的全部觸發器加起來就一個線程,再加上測試類一個線程,main一個線程,整個就三個線程,並無很好地利用多線程的併發性。函數

2.程序結構




此次做業坑在指導書,程序自己難度不算太大,結構也不算太複雜。性能

3.bug分析

  此次做業的bug主觀因素實在是太大,不少bug不是我本身程序寫的有問題,而是改需求改出來的。
  因爲我上面所述的處理方式,以犧牲併發性爲代價換取線程的安全性,基本上線程安全不會出現任何問題(至少通過我本身的大量測試沒有問題),可是效率上明顯不足,處理速度較慢,監控的文件一多就反應不過來,只能經過readme大法,強制要求兩條測試之間sleep一段時間,這段時間根據觸發器和監控文件的數目進行調整,至少sleep(1000),實際上我本身並無解決這個問題。還好給我測試的同窗遵照了這條規則,因此此次做業我也沒有被抓到bug。
  我測試的這份做業在公測階段就出現了大量問題,主要表現是對文件的各類修改徹底沒有反應。後來閱讀代碼發現這是因爲同一個問題形成的,因爲在全部觸發器線程start以後沒有作任何休息處理就啓動了測試線程,觸發器還沒來得及創建初始快照,測試線程就已經對文件進行了修改,因此就捕捉不到對應的變化,算做是一個bug。另外一方面這份程序對指導書的理解在有些地方有較大出入,好比summary輸出的是全部觸發器觸發的總次數,只能建立10個觸發器而不是10個監控路徑這兩個問題。我拿到的這份做業要求兩條請求之間sleep至少3s(比個人要求還過度...)這樣實際上就很難再找到線程安全有關的問題了,不過看起來大部分同窗都是經過這個方法來避免線程安全問題的,最後的結果實際上和我犧牲併發換取線程安全異曲同工,甚至效率還沒我高。(ps.經過寫入超大數據延長文件操做時間引起線程安全問題,我以爲不怎麼道德,我也沒用,不過經過文件操做加靜態鎖確實能夠解決這個問題)學習

4.心得體會

  如何在保證線程安全性的同時維持併發是此次做業讓我想的最多的一個問題。後來想的也許有點走火入魔,以爲安全性優先,再加上公測和互測對結果的準確度要求極高容不得半點隨機性致使的錯誤,併發性退居到很是靠後的位置,再加上某dalao的助攻,下次做業就走向了一條不歸路...

出租車

1.設計策略

  我最初的想法是100輛出租車開100個線程,調度器開一個線程,輸入開一個線程,基本模式與多線程電梯相仿,只不過把一維運動改成二維運動,可是仔細一想就以爲有問題,以前電梯開3個線程運行時間一長就有積累偏差,此次100輛出租車,偏差就更難以免了。這就引出了上面提到的第三個矛盾線程的數量與程序性能的矛盾。我在第三次OO上機實驗時嘗試過開100個線程的效果,大部分的時間都用在了上下文切換上,運行速度不比開5個線程快,並且此次做業出租車運行一格的時間僅有200ms,對性能的要求極高,不容許有卡頓現象的發生,200ms內必需要調整100輛出租車的狀態,而且還要進行相應請求的分配,開100個出租車線程顯得太多了,也有些不切實際。
因而就有走上了一條不歸路的說法。說是不歸路,但我以爲走好了也多是一條通天大道。這個方案來自某dalao。再回過頭分析一下以前的多線程電梯做業中的兩個問題,一個是假時間,一個是時間邊界,這兩個問題僅依靠多線程來處理是幾乎無解的,不免會出現偏差,這是多線程自身的隨機性所致使的。而測試不容許這種隨機性偏差的發生(這也算是課程要求的一個缺陷吧,但願之後在公測和互測階段可以有所改進),爲了確保萬無一失,須要一個全部狀態都是肯定的多線程。
  分析此次的指導書,最小時間單位爲100ms,出租車運行一格須要200ms。100ms主要是爲了區分請求,最關鍵的實際上在這個200ms上,出租車一開始從0ms時刻開始運行,每次全部出租車處於地圖上某個肯定的點上的時刻一定是200ms的整數倍,因爲每條請求實際上最終都要落實到出租車去執行,因此實際上請求開始執行和結束的時間也一定是200ms的整數倍。因此只須要每200ms掃描一遍出租車列表,掃描一遍請求隊列就能夠了,並且要確保掃描請求隊列的時候全部出租車的狀態都已經肯定了,因而就有了下面的方法。
  100輛出租車總體做爲一個線程,調度器做爲一個線程,每200ms掃描這100輛出租車,並調用每輛出租車改變自身狀態和位置的函數,在全部出租車的狀態都改變以後喚醒調度器,進行請求的分配,此時全部出租車的狀態都已經肯定了,絕對不會出現開一百個線程可能會出現的在分配請求時部分出租車已經完成了移動,另外一部分出租車還沒完成移動的狀況發生。調度器與出租車列表兩個線程進行了互斥處理,確保兩個線程交替進行,睡眠時間調整爲200ms減去其真實運行的時間,從而保證不會出現累計偏差。
  爲了進一步縮短運行時間,我將全部出租車掛在其位置對應的地圖點上,請求到來時,調度器只須要搜索請求出發點爲中心的25個點便可,免去了遍歷100輛出租車並進行比較的麻煩。另外一方面,我將地圖上任意兩點間的最短距離在初始化時提早算好,以後直接拿出來用。通過這一系列的處理,即便是同時有300條請求到達,在我這個低壓雙核i7上也能夠在50ms內完成遍歷出租車以及分配這300條請求的一系列操做,性能上至關強勁,同時因爲兩個線程的互斥處理安全性也獲得了保障。

2.程序結構




程序有四個類主要的類:
出租車: 須要記錄當前自身的位置, 狀態, 是否有接單, 若是接單, 出發點和目的地在哪, 還要記錄處於當前狀態的時間。
請求: 也就是用戶, 須要記錄出發點, 目的地, 發出請求的時間, 請求是否已被分配,是否已經執行完畢, 記錄請求窗口期中接單的全部出租車, 記錄最終選定的出租車。
地圖: 須要記錄出租車在地圖上的位置, 並記錄全部出租車的狀態。
調度器: 須要擁有地圖和請求隊列並記錄時間。
除以上四個以外還有其餘一些輔助的類, 好比 gui , 請求隊列, 輸入輸出處理類。
對象之間的交互:
調度器:
a.按請求隊列中的請求的出發點在地圖上尋找範圍內符合條件的出租車並分配給對應的請求。 200ms 一次。
b.實現將已經執行完的請求剔除。 200ms 檢查全部請求並剔除其中執行完的請求。
c.更新整個系統的時間, 保證其餘對象獲取到的時間是正確的, 200ms 一次。
出租車:
a.調整本身的運行狀態以及軌跡, 按照請求的要求進行移動。 200ms 一次
b.從地圖獲取最短路徑矩陣, 從而實現按最短路徑移動。 200ms 一次
c.從調度器得到當前時間, 200ms 一次
請求:
a.從調度器獲取搶單的出租車, 200ms 刷新全部請求一次
b.從搶單的出租車中選擇出最優的一個(即知足指導書要求的出租車)在到達窗口結束時間時
地圖
a.爲調度器查詢出租車的位置提供數據和方法支撐, 200ms 可供全部請求每一個查詢一 次
b.定時更新全部出租車的狀態信息, 週期爲 200ms
c.爲出租車的運行提供地圖鄰接矩陣以及距離矩陣
因爲該系統自己就十分龐大複雜,再加上提供的gui包結構混亂,致使整個度量結果看起來並不太好,類圖也十分龐大(若是看不清楚右鍵點擊查看圖片,能夠看清晰的圖片)。

3.bug分析

  此次做業因爲使用了上述的結構,避免了至關多的問題,不少其餘同窗遇到的麻煩問題在我這都迎刃而解,好比使用這個結構,不會有時間上的累計偏差,性能強勁能夠應對大規模的併發請求,全部出租車狀態肯定不會出現莫名其妙的問題。從寫完到最終提交實際上只是根據指導書的幾個細節理解對輸出格式以及部分狀態的調整時間作了改動,並且修改過程都至關簡潔,讓我體會到了一個好的設計能夠極大地簡化後期維護的壓力,並且在互測階段也沒有被發現bug。
  互測時我拿到的這份代碼,有幾個很明顯的問題,首先是命名規範,雖然大部分使用了英文命名,但其中有一些拼寫錯誤,妨礙理解。另外一方面,在函數中聲明的不少變量只是聲明瞭但並無使用,在沒有循環塊的狀況下常常出現沒有任何做用的continue語句。讀這份代碼確實難度極大,但由於要自行編寫測試代碼,仍是耐着性子讀下來了,讀代碼的過程當中就發現了一些問題,好比調度每分配一條請求後就會sleep(100),出租車每運動一步都sleep(200)。前者致使根本沒法處理多條同時到達的請求,後者會產生積累偏差。這兩個問題實際上均可以經過簡單修改就能夠避免。可是因爲每100ms纔會分配一條請求,極大地減小了系統的運行壓力,掩蓋了不少其餘的問題,沒有再發現其餘bug。

4.心得體會

  此次做業,前期因爲有dalao的幫助,在設計模式上下了很大功夫,作好前期設計後期正式開始寫代碼內心也就有了譜,相比IFTTT淡定了許多。我對課上所講的設計模式也有了更深的體會。100輛出租車開一個線程也確實給我開啓了一個新思路。

總結

  這三週能夠說是我二十年來過得最痛苦的三週,高三都沒有這麼忙過,在OO和OS的雙重壓迫以及一堆瑣事的干擾下,幾乎天天晚上睡覺的時間都在凌晨一點半以後,週一週二基本上都是凌晨兩點半以後,沒有周末,甚至連清明假期都沒過,就這樣處於高度緊張狀態持續了三週,中間幾乎沒有任何休息,常常在猝死的邊緣試探,能夠說也是對本身意志的一種磨練。我之因此可以度太重重難關,最主要的緣由其實是有衆多dalao的幫助。從指導書理解,到bug調試,再到測試數據設計,在探討的過程當中經過你們一塊兒努力發現問題分析問題解決問題,各類新奇的想法互相權衡比對,優中選優。最後就不一一 @ 各位dalao了,謹在此致以衷心的感謝。

相關文章
相關標籤/搜索