OO第二次博客

 

oo5_7html

多線程同步策略分析

1.多線程電梯時的策略

線程分析

多線程電梯時,我還執着於時間的精準性,也就是上下樓必定要多少多少秒,因此採起的是假時間策略。java

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

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

同步策略

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

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

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

第一類同步問題,由於我採用的是電梯線程內部假時間的策略,因此很容易解決,只須要利用一把調度器線程與電梯線程共享的「運行鎖」便可。電梯線程每次主循環開頭獲取鎖,主循環末尾釋放鎖。當調度器線程但願中止電梯線程時,只需獲取該鎖便可。同時爲了不飢餓問題,這裏我採用了公平鎖,在只有兩個線程爭奪該鎖的狀況下,性能損失仍是能夠接受的。緩存

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

2.IFTTT時的策略

線程分析

線程劃分很是明細、簡單:ruby

  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,可是這課的設計就不是以給學生成績爲核心的。

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

相關文章
相關標籤/搜索