1. 程式人生 > >OO第二次博客

OO第二次博客

消息隊列 fields 根據 阻塞隊列 cor 放棄 分代 學生 代碼

oo5_7

多線程同步策略分析

1.多線程電梯時的策略

線程分析

多線程電梯時,我還執著於時間的精準性,也就是上下樓一定要多少多少秒,所以采取的是假時間策略。

為了實現假時間策略,我將三部電梯的運行封閉到了一個線程當中,單獨一個線程內部的執行是不會受到線程調度產生的誤差的影響的。

在這基礎上,考慮到輸入IO會有阻塞,安排了一個輸入線程。至於調度器線程,對於使用假時間策略的我的設計而言,它是可有可無的,畢竟實際的調度都是在電梯線程內部進行的,如果不在電梯線程內部進行,那麽調度與執行之間就會產生線程調度導致的時間誤差,會導致正確性問題。故而我的調度器線程雖然按照指導書要求而加上了,但它僅進行部分不會產生時間誤差的調度功能。

同步策略

在輸入線程與調度器線程之間僅有指令需要傳遞,我才用了一個阻塞隊列來實現指令隊列。利用java庫中自帶的同步容器類保證線程安全。

在調度器線程與電梯線程之間,存在兩類同步問題:

  1. 為了避免因時間誤差而產生正確性錯誤,我需要在同一時間獲取三部電梯的狀態,並且能在該時間將指令傳遞給電梯線程。
  2. 我需要保證電梯的狀態變更能在調度器線程需要訪問的時候就能被反映出來。

第一類同步問題,因為我采用的是電梯線程內部假時間的策略,所以很容易解決,只需要利用一把調度器線程與電梯線程共享的“運行鎖”即可。電梯線程每次主循環開頭獲取鎖,主循環末尾釋放鎖。當調度器線程希望停止電梯線程時,只需獲取該鎖即可。同時為了避免饑餓問題,這裏我采用了公平鎖,在只有兩個線程爭奪該鎖的情況下,性能損失還是可以接受的。

第二類同步問題,因為當調度器線程訪問電梯狀態時,電梯線程必定停止,不可能更新自身狀態,故而僅需確保電梯狀態都能反映到內存中,而不是被緩存。故而我大量使用了volatile變量、原子變量實現輕量級的同步。

2.IFTTT時的策略

線程分析

線程劃分非常明細、簡單:

  1. 觸發器線程組
  2. summary線程
  3. detail線程

同步策略

首先考慮觸發器線程組,它們之間共享的是被監控的文件,而這份線程安全性被委托給了FileCenter這一線程安全的File類的封裝類。

再考慮summary線程、record線程,它們之間沒有共享,但各自都和許多觸發器線程共享了它們記錄的信息。這是典型的“讀者-寫者”的情況。寫者是一堆觸發器線程,讀者是需要將記錄的信息寫進文件的summary、record線程。我采用了消息隊列的方法保證了寫者與讀者的同步,將線程安全性委托給了java的同步容器類。

3.第一次出租車時的策略

線程分析

這一次我拋棄了多線程電梯時註重正確性的策略,沒有采用假時間策略。故而這裏100個出租車不再只有一個線程,而是真正的100個線程。

同時,我註意到對乘車請求的響應、“搶單窗口”的設計非常適合使用服務器模型進行實現。故而我安排了一個線程池,這個線程池中一個線程對應於正在處理的一個乘車請求,稱該線程為調度單元線程。

除此之外,就還有一個標配的輸入線程。

同步策略

首先,出租車之間共享地圖,以及調度單元。

  1. 因為地圖在這次作業中是不可變的,故而線程安全性被保證。
  2. 對於調度單元,我采用了消息機制來處理出租車、調度單元之間的同步問題。即兩者都具有消息隊列,互相傳遞信息時僅通過消息隊列進行。這樣雖然降低了性能、正確性,但簡化了實現邏輯。

其次需要保證出租車的狀態對其他線程都可見,這一點通過簡單的內部鎖即可實現。

最後因為需要通過位置、狀態來訪問出租車,故而我安排了一個緩沖用的TaxisMonitor,出租車監控類來存儲緩存信息。因為該緩沖對象會被所有出租車訪問來更新緩存,故而需要進行同步。這裏我采取了細粒度加鎖策略,畢竟本身就是為了性能而做的緩沖,不能因為加鎖反而損失性能。對於每一個位置上一個鎖,每一種狀態上一把鎖。

不過因為每一次出租車狀態更新會需要訪問前後兩個狀態,如果同時獲取兩個狀態的鎖,會導致死鎖問題。我的解決方法是讓程序同一時間要麽只獲取前一狀態的鎖,要麽只獲取後一狀態的鎖,雖然會導致出租車在一段時間內在緩沖區中不可見,但可以簡單解決了死鎖問題。而不可見導致的正確性損失,在這次作業中並無傷大雅。如果真的會因為這點時間的不可見而產生正確性錯誤,那出租車線程本身運行的時候就會因為過卡而導致走一條邊超過200s了。

度量分析

電梯

技術分享圖片 這次電梯作業光看度量的面板數值還可以,也就輸入處理那裏我圖省事嵌套多了點。但是實際上因為是多次更叠的項目,其中有眾多冗余的代碼。這點從55個類、2898行代碼中就可以看出。

IFTTT

技術分享圖片 從面板數值上可以看出,這次IFTTT作業最大的問題就是,它的分支判斷相當地龐大。這一點我實在想不出怎麽避免,我已經將分支判斷盡可能封裝在一個方法中,並且保證該方法的接口統一性。或許可以采用將分支判斷數據化,然後編寫自動進行分支判斷的代碼來解決。

出租車

技術分享圖片 狀態變化——這是這次出租車的紅點。我在思考能否通過將狀態本身也給抽象出來,作為一個類對待?然後狀態變化的邏輯交由狀態本身來處理?或許這樣就能將復雜的圈邏輯降維,分散到各個狀態類中去。

類分析

電梯

技術分享圖片 從LiftsThread以右下的那一部分代碼全部都是和單線程時一致的,也就是電梯內部依然是按照單線程時的運作模式進行運作,不再贅述其內部實現。

為了能夠復用單線程時的代碼,我在LiftsThread內部,將系統時間的流逝轉化成了對Lift響應模擬時間變化的調用次數。也就是每過幾幾秒就調用一次Lift一個時間粒度的變化函數。

所以實際上,LiftsThread僅僅只是一個用來封裝模擬時間的線程,其內部不包含任何調度邏輯。

實際的調度邏輯全部被包含在Schedular及SubSchedular中。其中SubSchedualr為單部電梯時的調用邏輯。Schedular為協調三部電梯的運動量均衡策略邏輯。

SchedularThread僅僅是為了迎合指導書要求而贅寫的中介線程。

InputThread負責讀入指令,並將其放入CommandTray中。之後SchedularThread從CommandTray中取出指令,再暫停LiftsThread,轉交給它指令。

World負責系統內時間的管理。

IFTTT

技術分享圖片 ifttt中我大量使用了繼承,主要原因是指導書的不明確以及來自助教的需求的頻繁變更,導致了代碼需要不斷維護。為了減少代碼維護時的工作量,我盡可能地復用代碼,減少同質代碼的出現。

繼承樹一共有四支。

  1. Trigger樹。Trigger即為觸發器,每一個Trigger都是一個實現了Runnable的可運行類,在實際的程序運行中,每個Trigger都是一個監控線程。其工作即為每隔一段時間獲取監控對象的快照,隨後根據自身響應方法的具體實現,生成Alter,交給註冊在自己身上的Task處理。
  2. Task樹。Task即為任務。Task負責接收文件快照的變化(Alter),隨後根據自身響應方法的具體實現,進行處理。
  3. Recorder樹。Recorder負責信息的記錄與記錄文件的讀寫。其自身也是一個線程,每隔一段時間就刷新文件中的記錄。
  4. Test樹。為了方便測試者進行測試,我將較為具體的測試時的運行邏輯封裝在了抽象基類Test類中。每一個具體實現了Test類的類都可以作為一個測試樣例執行。

FileCenter即為一個線程安全的File類的封裝類,負責文件讀寫的底層封裝。

出租車

技術分享圖片 出租車的類設計主要分為了三個族:

  1. Taxi族。這部分包含了Taxi, Driver, TaxisMonitor。這一族內部高度耦合,三位一體地實現了出租車。

    1. TaxisMonitor負責出租車狀態的對外查詢,其內部實現為緩沖實現。每當出租車狀態發生更新時就會調用該對象,進行緩沖更新。
    2. Taxi負責出租車的運動邏輯的維護。也就是出租車實際在地圖上如何位移的問題。
    3. Driver負責出租車的服務邏輯的維護。也就是出租車如何與調度單元進行消息交互的問題。

    Timepasser為Taxi的基類,封裝了Taxi的線程邏輯。負責將系統時間的流逝轉化成Taxi內部時間的每時間粒度的流逝。

  2. Schedule族。這部分包含了ScheduleServer, ScheduleUnit。ScheduleServer維護了一個線程池,該線程池中每一個線程都為ScheduleUnit的實例化。這一族負責響應乘車請求,並對每個乘車請求分配一個線程進行搶單、分單等邏輯。

  3. Map族。這部分包含了Map, RouteSolver。這一族封裝了地圖的具體實現。

除了這三族以外,還有幾個類是為了之後的擴展而額外實現的:

  • TwoSideMessage。給消息增加了一層抽象,使得之後還能擴展出除了乘車請求交互時的消息以外的消息。
  • Mesable。該接口是為了使得之後其他類也可能能夠收發消息而實現的。
  • InputProcessor。該類將輸入處理與乘車請求響應分離開來,是為了之後增加除了乘車請求的輸入。

設計分析

關於這個,我難以進行分析……因為我不知道我有沒有做到。究竟做到什麽程度才能算是做到了某一項原則?我沒有足夠的經驗去回答這個問題……我只能說我盡量去做了。

bug分析

主要的bug來自於對指導書理解有誤以及未及時看issue和微信群。其次的bug大多是因為我沒寫正則去檢查輸入是否合法,只要輸入錯了,那我就挑正確的然後繼續跑,跑不下去再跑錯,但是公測要求不管能不能繼續跑都要報錯。

我沒怎麽查別人的bug,沒那個時間,僅僅只是按照公測要求完成了課程安排的工作。

心得與體會

其實多線程的同步控制並不困難,與電梯復雜的調度邏輯相比,那是很簡單的東西。代碼編寫過程中最大的難點其實是兩點:

什麽是正確的代碼?

我很想說我知道這個問題的答案,但我做不到。僅僅只是閱讀指導書,進行需求分析,並不能真正導向“正確的代碼”,而只是能接近。在知道這件事情以後,我所能做的或許僅僅只是留出“將代碼改正確的余地”。

性能 || 簡單

在程序寫完前我無從得知程序的性能如何——這很顯然,但是我卻必須要寫完它。如果我在編寫過程中就考慮性能問題,那很有可能就會導致我代碼根本寫不完,或者越寫越錯。因為優化性能的代碼邏輯往往是復雜的。在第一次編寫的過程中我往往會采取最簡單的那種方法,而不是最快的方法。我所能做的或許僅僅只是留出“將代碼變快的余地”。

總結

上述兩個問題最終都導向一點:余地。也就是代碼的可修改空間、可拓展空間。當我編寫的程序不只是寫完就行,過了OJ就行的時候,“余地”成了我編寫代碼時需要重點考慮的因素。

對課程的看法

在電梯的時候,我十分執拗於程序的正確性,通過不斷地發issue明確指導書的意思,我雖然不能完全做到,但是卻能向“正確的程序”接近。

不過在ifttt時,需求頻繁的變更、指導書的不明確讓我放棄了對正確性的執拗——那是一件做不到的事情了,同樣一份代碼,兩個人看,一個人覺得對,另一個覺得錯,不再有統一規定。

公測逐漸轉為互測,互測時bug樹可以用一個bug掛滿,許多人的公測因為對方沒測而滿分,吐槽版各種認領代碼,等等現象,讓我明白了一件事情:這課的成績沒有很大的意義,就和我的博雅課程的成績的意義差不多。雖然這課的成績會算進GPA,但是這課的設計就不是以給學生成績為核心的。

言盡於此,並不是抱怨,這就是我所見到的,我所想的。

OO第二次博客