BUAA_OO_2020_第二單元總結


 

BUAA_OO_2020_第二單元總結

第一次

設計策略

本次作業采用生產者、消費者模式設計,大致框架如圖所示:

  • 生產者:輸入線程

  • 消費者:電梯線程

  • 托盤:Dispatcher調度器

線程安全方面,調度器中的指令隊列為輸入、電梯線程共享對象,需要保證其線程安全。

調度器中包含synchronized關鍵字修飾的put(), get()方法。InputHandler線程作為生產者,調用put()方法向隊列中添加請求;電梯線程作為消費者,調用get()方法獲取請求並執行。

結束線程策略為,當輸入結束后,輸入線程向調度器傳遞stop信號后自行結束。再由調度器向電梯傳遞stop信號,電梯在指令運行結束后自行結束。

 

調度算法

調度策略采用look算法,即在某一方向運行時,若當前層有請求,且請求的運行方向與當前相同,則開門接收乘客。若電梯內沒有乘客,且前方沒有新請求,則轉向。

基於度量的代碼結構分析

  • 代碼結構

    第一次作業要求實現單部多線程可捎帶電梯,初次接觸多線程,代碼結構比較簡單,UML類圖如下:

第一次作業結構相對簡單,總共分為四個類:主類、輸入類、電梯類、調度器類。

時序圖如下:

程序基本運行流程:主線程啟動InputHandler和Elevator線程,Elevator線程在獲取到指令之前處於wait狀態,直到獲取指令並執行。在輸入結束后,由InputHandler發出結束信號,即置Dispatcher中的stop信號位true,並自行結束。當電梯指令全部執行完畢且獲取到stop信號為true時,自行結束。至此,程序自行結束。

  • 代碼復雜度

    • 方法復雜度

    • 類復雜度

    •  依賴矩陣

    本次作業代碼,所有方法都控制在35行以下,因為我對於代碼行數的控制有了明確的意識,盡量對於一部分功能進行拆分,使得代碼結構更加清晰,且方便模塊檢查。

    根據設計原則,run()方法瞄准Main函數,簡潔為主,僅描述重要步驟,但其復雜度相對較高,經檢查,是由於其涉及結束線程的判斷部分,if-else結構相對其他方法較多。可以考慮將判斷是否結束線程單獨抽離為一個方法來減少其復雜度。

    類復雜度都在合理范圍內。

    本次沒有出現循環依賴情況,調度器和電梯之間關聯較大,這是由於本次作業我暫時沒有采用分調度器的方式,而是將指令隊列直接放在了總調度器里,從而造成電梯和調度器之間的頻繁信息交換。

關於測評

本次作業沒有在公測中出現bug。

在互測中被黑出一個bug。由於初次接觸多線程,對於輪詢的含義理解不清,沒有規避導致電梯在運行到中途等待下一指令到來時出現輪詢,不斷while循環判斷是否還有指令,導致CTLE。后經過修改,使得電梯在運行中途暫時沒有指令時進行等待,有新指令到達后再喚醒,修正了bug。

 

第二次

設計策略

依然采用生產者-消費者模式

  • 生產者:輸入線程

  • 消費者:電梯線程

  • 托盤:Controller總控制器

本次作業開始涉及多部電梯,因此對代碼的結構層次進行了調整。首先,新增Controller類,作為總控制器,而每個電梯都對應一個屬於自己的Dispatcher,只負責單個電梯的調度。另外,為了方便對於每個乘客的信息進行記錄,新增Person類。

另外,本次作業首次出現電梯承載人數限制,為防止超載,我采用的方法是在電梯類判斷,即電梯到達某一層查詢是否接人時,考慮當前電梯剩余空位,若還有余位,則上人,否則忽略。

線程安全方面,指令序列作為共享對象,需要保證線程安全。Person類中有關人員狀態的屬性可能同時被多個電梯線程訪問,也需要保證線程安全。另,為了保證正確輸出,新增Output類實現線程安全輸出。

值得一提的是,經過理論課的學習,我了解了讀寫鎖這一特殊加鎖方法,並在本次作業的Person類上予以嘗試。即讀操作間互不影響,但寫-寫,寫-讀則被鎖住。經過對拍比較讀寫鎖synchronized直接修飾方法兩中模式的運行時間,發現性能並無提升(其實有不少點還更慢了)。究其原因,本次作業情景的讀操作頻率並沒有明顯大於寫,導致讀寫鎖的優勢沒能發揮出來,反而由於實現讀寫鎖的時間成本較高,導致性能不升反降(僅為個人猜測,若有錯誤請指教)。因此最終還是采用synchronized關鍵字加鎖。

調度算法

“無為而治” 出自《道德經》,是道家的治國理念。它並不是什么也不做,而是不過多地干預、充分發揮民眾的創造力,做到自我實現。

自本次作業涉及多部電梯調度開始,我選用的調度算法融合了無為而治的精神內涵,即控制器不對指令的分配有過多約束,而是發揮各電梯的自我能動性。具體分配細節如下:

  1. 將讀取的指令加入每一部電梯的隊列中

  2. 依舊基於look調度算法,當某一部電梯要執行該乘客的指令時,將Person類中的Taken屬性設為true,表示該乘客已經進入某個電梯中。

  3. 所有電梯在遍歷查找需要執行的指令時,需要判斷該乘客還未進入其他電梯,即Taken=false,才考慮加入該乘客。

綜上,可以生動地總結為搶人分配方法。通過后期和同學們的討論來看,這種方法在隨機生成的數據時比較吃香,因為基本保證乘客盡快進入相應電梯。但缺點是比較浪費電梯資源,會出現多個電梯哄搶1人情況。

該分配方法對於代碼復雜度也有影響。由於電梯沒到一層,遍歷指令序列時,都需要對Person類的Taken進行一些處理,這導致Person類對電梯類、調度器類的依賴都比較深。

除了該分配方法,我還嘗試以下兩種分配方法,由於性能相對落后,最終沒有采用。

  1. 隨機分配:按照指令進入順序S型分配進個電梯

  1. 最近分配:查找與當前指令起點邏輯距離最近的電梯進行分配。邏輯距離:綜合電梯當前位置、請求起點的位置、電梯運行的方向、請求方向計算得出的電梯接到該乘客之前移動的距離。

 

 

基於度量的代碼結構分析

  • 代碼結構

    第二次作業要求實現多部多線程可捎帶調度電梯,UML類圖如下:

     

    時序圖如下:

  • 與第一次作業相似,由InputHandler讀入請求,並加入Controller中的指令隊列。Controller將指令按照一定規律下發至各個電梯,並加入至對應電梯的調度器中的待執行請求隊列。電梯從Dispatcher中獲取指令並執行。當輸入結束,InputHandler向Controller發出結束信號,自行結束。Controller再將結束信號下發到各電梯。由每個電梯自行判斷,當自己任務已經完成,自行結束。至此,程序自行結束。
  • 代碼復雜度

    • 方法復雜度

    • 類復雜度

    • 依賴矩陣

    本次作業在第一次作業的基礎上進行擴展,方法復雜度的情況與第一次作業基本相同,依然時電梯run方法復雜度高於其他方法。getID()涉及到通過i=0,1... 分別返回 "A", "B"...,存在五個if-else分支,因此 ev(G) 較高。電梯基於代碼合理拆分,本次作業依然保證代碼行數都在40以下。

    由依賴矩陣可見,和Person類的關聯度整體較高,這是調度算法的涉及造成的,將在以下調度算法部分闡述。

關於測評

本次作業沒有在互測和公測中出現bug。

在互測中,用一個邊界數據(同一時刻30人同時從1樓出發)黑到兩位同學,都是在執行完指令后線程沒有自行結束,導致RTLE。但本來是想黑A,B同學的,最后黑到的卻是A,C同學,由此可見多線程的不穩定性,bug關鍵時刻復現與否,還是看臉。

本次作業,我屋內一共成功五刀,全是RTLE。由此可見,如何讓線程正常地自行結束是多線程編程的難點之一。

 

第三次

設計策略

本次作業的基本策略與第二次作業完全相同。幾點不同如下

針對換乘策略,我將需要換乘的指令拆分為兩部分,首先向Controller中的請求隊列中裝入第一部分,下發至能夠到達該請求起點和終點的所有電梯。

不同電梯的屬性,根據創建電梯的構造函數參數改變來創建不同種類電梯。

結束線程策略,需要更新原先的方法,因為存在輸入結束且當前電梯沒有未結束的請求,但是可能在其他電梯運行過程中新增換乘請求的情況。更新為:控制器不僅要下發輸入結束的信號,還要向某一電梯下發其他電梯的狀態。若輸入結束,且其他電梯都沒有未結束指令,且當前電梯沒有未結束指令,則自行結束線程。

線程安全方面基本要求延續上次作業即可。

另,由於性能判定增加了人員等待時間,所有應當保證上電梯者在最后一刻上,下電梯者一開門就下。

調度算法

對於單個無需換乘的指令,調度算法與第二次相同,即所有別分配到該指令的電梯進行搶人

對於換乘指令,我統一以1、5、15三層為換乘點,防止換乘點過多導致混亂或開關門過於頻繁等問題。在Controller類中預處理所有換乘情況,構成換乘矩陣,在讀入請求后,根據矩陣的對應元素直接獲取換乘方法。

SOLID原則

  • 單一責任原則

    • 輸入類僅負責讀取輸入並加入控制器和向控制器發送結束信號;

    • 總控制器僅負責將指令按調度需求分配給電梯;

    • 電梯負責將指令裝入調度器、從調度器取指令、給控制器返回電梯狀態。其責任略多。

    • 調度器僅負責存儲電梯指令、指導電梯進行掉頭、前進等操作。

  • 開放封閉原則

    • 使用的電梯類延續自第一次作業,幾乎無改動。多個電梯即對電梯類進行多次實例化。

    • 輸入線程延續自第一次作業,根據輸入要求略加修改對於 “托盤” 的方法調用。

    • 調度器延續自第一次作業,幾乎無改動。

    • 控制器改動較多,多次作業迭代的改動基本都歸於控制器方法的增減。

  • 里氏替換原則:未涉及繼承。

  • 接口分離原則:僅有線程實現Runnable接口。

  • 依賴倒置原則:出現電梯類和控制器類相互依賴情況,是設計策略需要所致。

在功能與性能平衡上,首先滿足功能正常,並采用較為簡單的分派策略,並沒有在控制器內大篇幅進行判斷分配指令,從而保證了控制器邏輯相對簡單,預防bug的出現。

基於度量的代碼結構分析

  • 代碼結構

    本次作業要求實現動態添加電梯且電梯到達層數有限制的多部多線程可捎帶調度電梯。UML類圖如下:

  • 本次作業在類的層次和第二次作業沒有任何差別,改動了Controller中的部分方法,並增加Person類中有關換乘的屬性。
  • 時序圖如下:

  • 與第二次作業類似,新增在電梯在執行完指令后,判斷是否還有換乘的下一部分,若有,則將后半部分指令加入請求隊列,重復上述過程。

    結束過程已在設計策略中提到,不再詳述。

  • 代碼復雜度

    • 方法復雜度

    • 類復雜度

    • 依賴矩陣

  • Controller類的getCategory()方法ev(G)較高,因為其涉及根據請求的起點、終點返回換乘情況矩陣的下標,因此if-else分支較多。

    延續前兩次作業,本次作業依然控制單個方法規模在30行以下。

    電梯類的WMC超標,且電梯類的代碼規模偏大,可見電梯類承擔的責任較多。經分析,除了電梯運行的相關函數,進行Controller和Dispatcher之間的信息溝通也全部經由電梯實現,這導致電梯中方法數量較大。

    本次作業出現循環依賴情況,這是由於電梯類和控制器類之間存在雙向的信息交換,即控制器向電梯下發指令和結束信號,電梯向控制器返回當前運行狀態,用於判斷是否能結束線程。

    Person類相關的關聯深度依然較高,原因與上次作業相同,在控制器、調度器中都需要經常訪問乘客的相關信息。

關於測評

本次作業沒有在共測中出現bug。

在互測中,被我本地測過n次的幾乎一模一樣的數據黑出ctle,無法復現,再次提交即通過。經分析,由於我考慮測評機會在指令放完后立即^D,因此沒有處理當所有電梯都等待新指令時直接^D的情況。修改后提交也可通過。但該測試點其實也沒有涉及到這個情況,所以我依然很迷惑。

在互測中,黑到兩位同學。一位同學在最后一個需要執行的指令需要換乘時並沒有成功換乘,而是在換乘點直接結束了。這便是電梯的結束條件判斷有誤,需要執行該換乘請求后半部分任務的電梯線程誤以為不會再有新的指令而錯誤地結束了,導致換乘后請求沒有執行。另一位同學只能處理新增電梯編號為X1,X2,X3的情況,是指導書要求誤解造成的。

 

心得體會

線程安全

線程安全,萬惡之源。有了他以后,出現WA都很開心。

在本單元的學習過程中,我在第三次作業書寫過程中出現線程安全問題,原因為Controller類中的方法加鎖不充分,即並沒有對所有訪問請求隊列的方法進行加鎖導致。另外,若所有電梯都在等待新指令到來時直接^D,所有電梯都會處於wait狀態。

結合同學們的交流和我個人測評感受,本單元的一大難點就是第三次作業電梯線程的自行結束。由於結束條件變多,若缺失考慮,則會造成互測中被黑到的同學一樣的線程提前結束錯誤;若沒有處理好結束條件之間的關系和電梯等待條件,很容易造成所有電梯線程都陷入等待的死鎖情況。

設計架構

本單元重點在多線程,我的代碼實現中除了對Runnable接口的實現,沒有其他繼承實現關系,因此代碼層次比較簡單。

對於生產者-消費者模式的應用,三次作業迭代經歷了單一消費者到多消費者的過渡,實驗課上也體驗了多生產者&多消費者的過渡,讓我深刻體會到了設計模式的擴展過程和方法。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM