『BUAA-OO-Unit3-Summary』
------------恢復內容開始------------
『BUAA-OO-Unit2-Summary』
Homework5
0. 寫在前面
由於沒有看清DDL,導致本次作業卡點提交未能成功通過。(悲)
作為多執行緒程式設計的初學者,我認為本次作業的難點有三:
-
wait()/notify()
的使用; - 共享物件類的構造;
- 排程策略的程式碼實現;
接下來,我將對本次作業的實現過程進行一個大致的復現,並對以上難點著重展開講述。
1. 重點關注:同步塊/共享物件/執行緒安全類
關於多執行緒程式設計,在拿到題面/瞭解客戶需求後,設計架構時要提前想好以下幾個問題:
- 需要設計哪些執行緒類?
- 共享物件是誰?
- 執行緒與共享物件的關係是什麼?
對於第五次作業來說,我們需要以下幾個執行緒類:
-
InputThread
:輸入執行緒類,負責對標準輸入的乘客請求進行解析,並將其傳遞給Dispatcher
執行緒類。 -
Dispatcher
:任務分配器,根據起點將各乘客分配到不同的請求佇列中。 -
Elevator
:電梯執行緒,將乘客運送到相應樓層並輸出 執行軌跡 與 乘客資訊。
如果分別構造InputThread
與Dispatcher
兩個執行緒類的話,那麼還需要實現一個供二者進行資訊互動的共享物件類。但是二者均屬於單例模式類,也就是說,自始至終只有兩個執行緒會來訪問這個共享物件,並且一個只會寫,一個只會讀。這樣的設計顯然十分不合理,因此我將InputThread
Dispatcher
合併為一個執行緒類並命名為Dispatcher
,其功能為從標準輸入中獲得乘客請求並將其分配到對應的佇列中(即二者功能合併)。
至於共享物件,就十分明顯了:Dispatcher
需要將乘客資訊寫入一個佇列,而Elevator
類的五個例項(5 elevators
)則需要從該佇列中選擇自己的任務來工作,並在任務完畢後將該任務從佇列中刪除。因此,共享物件就是各樓座的等候佇列,建立共享物件類WaitQueue
。WaitQueue
的內部結構,我選擇了同時維護一個ArrayList<>
與一個ArrayList<ArrayList<>>
,維護ArrayList<ArrayList<>>
ArrayList<>
則起到了保留請求到達先後順序的作用。採用這樣看起來有些複雜的結構實際上有一個好處:明面上提示了設計者需要使用同步塊。尤其是對於當時剛剛接觸多執行緒的我來說,其實對共享物件讀寫的保護意識並沒有那麼強烈,但是在add
與remove
WaitQueue
中請求時總是要同時對兩個資料結構進行操作的這個事實總是在不斷地提示著我讀寫方法的非原子性,共享物件的讀寫方法必須加synchronized
同步塊保護才能線上程中放心大膽地任意使用。
- 關於共享物件類的構造:其實保證共享物件類內方法的安全性有兩種方法:第一,開發者保證共享物件是執行緒安全類,即在構造時保證方法的原子性(不可分割的方法);第二,在呼叫每一條方法前,使用者對該指令操作的物件進行同步保護。只是,應該沒有人會智障到採用第二種方式設計產品,或者說,應該沒有使用者會智障到選擇採用第二種方式製作出來的產品吧。
各執行緒與共享物件的關係?Dispatcher
執行緒對WaitQueue
進行寫入操作(W),而Elevator
既要從WaitQueue
中讀取資料,還要在任務完成後將資料清除(RW)。
這幾個基本的問題思考清楚以後,我們便可以開始動手實現了!
2. 排程策略
關於排程策略,雖然學長與助教強烈推薦了LOOK
演算法,但我還是倔強地使用了ALS
策略。
說實話,其實ALS
策略並不是很好寫。主要概念只有兩個:主任務與捎帶任務。實現分四步走,思路如下:
getMainMission();
move();
getNextMovingDir();
outAndIn();
-
getMainMission() / move()
存在兩種情況: -
A. 若電梯載客佇列為空,則將
WaitQueue
中到達時間最早的請求作為主請求,朝著其起點方向移動; -
B. 若電梯載客佇列不為空,則將電梯載客佇列的第一個請求作為主請求,朝著其終點方向移動。
-
P.S.
ARRIVE-*
資訊在move()
中列印 -
getNextMovingDir()
:- 若到達主請求的起點樓層(A1),
nextMovingDir
即為向主請求終點移動的方向 - 若未到達主請求的起點樓層(A2),
nextMovingDir
仍為向主請求起點移動的方向 - 若未到達主請求終點樓層(B1),
nextMovingDir
為向主請求終點移動的方向 - 若到達主請求終點樓層(B2),
nextMovingDir
為0,損失一次進人機會(該方法的一個小漏洞)
- 若到達主請求的起點樓層(A1),
-
outAndIn()
:- 為在不超載情況下最大限度多拉客,先
getOut
後getIn
。 - 為了將判定乘客進出邏輯與列印
OPEN-*/OUT-*/IN-*/CLOSE-*
兩個過程分開實現(更加清晰),先獲取出進成員名單,後打印出進結果。
- 為在不超載情況下最大限度多拉客,先
至此,排程策略講解完結。
3. 架構模式(類圖/SD圖)
4. Bug修復
本次作業中測未能成功通過,CTLE
的原因即為沒有完全搞懂wait()/notifyAll()
的實現機制,導致共享佇列為空時反覆輪詢。對此漏洞進行修復後,在第六次作業強測中又一次出現了CTLE
的問題。這個問題一會兒再細說。
並且第一次堆砌的自以為實現了ALS
策略的ShitMountain
複雜度太高,應該也是導致超時的重要原因。
Homework6
0. 寫在前面
本次迭代要求新增兩大亮點:
-
維護原有縱向層際直線運載電梯,新增橫向座際環形觀光電梯(北航特色)
- Too much character, an access of character. --- Elizabeth Alexandra Mary Windsor
-
同座/同層可支援多臺電梯
1. 重點關注:執行緒安全/wait()/notifyAll() [詳見Bug修復板塊]
本次作業難度真的不是很大。但是由於沒能成功參與第五次作業的互測,導致第六次作業強測中出現了本該在上一次作業中解決的歷史遺留問題。
首先,先來談一談環形電梯的實現:
- 我居然真的傻傻地按照指導書的要求,對環形電梯載客的最優策略進行了路線設計,好一陣取模運算比較選擇最優執行方向後,在研討課上和大家交流時才恍然意識到:環形電梯一共只有五個停靠點,即使電梯始終按同一方向執行也不會損失很多時間。仔細一想確實如此,並且在現實生活中的環形電梯也確實是始終朝著一個方向執行的;如果停靠點數目過多的話,我猜測設計者應該也會採用地鐵環狀線內外環兩路的運營方式,而不會讓電梯的執行方向始終飄忽不定的。
廢話了這麼多,第六次作業中我的環形電梯採用了執行方向可變的最優排程模式。由於實現方法與縱向電梯較為類似,只是計算最優路徑時增加了一些取模運算,這裡不再贅述。
而多臺電梯的問題,我還真的思考了很久沒有想到一個最佳的排程方案。後來無意之中提交後奇蹟般通過後才意識到讓共享同一佇列的電梯間自由競爭居然也是一種解決方式。
2. 思考:HiddenDispatcher(隱式任務分配)
廢話少說,直奔主題:
前一段時間我一直在思考這個問題,電梯執行緒間的資訊是不能共享的,那麼如何實現共享同一WaitQueue
的執行緒間任務的最優分配呢?
e.g.
1-FROM-A-7-TO-A-8
, 2-FROM-A-8-TO-A-10
, 3-FROM-A-2-TO-A-1
;其中,A
座共有兩臺電梯,一臺(1)停在6層,另一臺(7)停在3層。
採用傳統的自由競爭策略,必然會導致4號與7號電梯共同競爭上行的兩個請求,7號電梯節節敗退,白白跑到8層,一個請求都沒有接到,而3號請求卻始終無人問津的窘況。
為了破解這一難關,並且避免電梯執行緒間資訊共享,就需要構建一個可以同時獲取WaitQueue
資訊以及共享這一WaitQueue
的全部電梯當前所在樓層及執行方向的Dispatcher
。雖然聽起來是一個好主意,讓人歎為觀止;但是聽起來也有一點麻煩,讓人望而卻步。
但是轉念一想,自由競爭方案中難道就不存在任務的分配器了嗎?即使只考慮一個電梯執行緒,在進行主任務獲取時,getMainMission
方法在傳入引數中仍然獲得了當前電梯的所在樓層,getRequest()
方法不僅獲得了當前電梯的所在樓層,還獲得了執行方向。而兩個方法都是WaitQueue
中的方法,也就是說我完全可以將這兩個方法獨立出來,形成一個MissionDispatcher
執行緒幫助電梯確定主任務,以WaitQueue
與mainMission(Elevator)
為共享物件,將二者關聯起來;換句話說,這樣的一個MissionDispatcher
執行緒類如果只針對單一電梯執行緒進行任務分配,那麼完全可以將MissionDispatcher
的全部功能簡化為method
方法,歸併入WaitQueue
的一部分。
就這,我願意稱之為隱式任務分配(Dispatcher hidden in methods)。
3. 架構模式(類圖/SD圖)
4. Bug修復
兩個漏洞:
- 第五次作業的殘留問題:沒有保護輸出執行緒(不安全),需要套上
synchronized
語句塊。(看了討論區一位同學的分享,講的很清楚,這裡不重複) - 關於
wait()/notify()/synchronized
的實現機制:- 我對這個問題一直有一些誤解,以為被
synchronized
獲得的鎖在跳出語句塊釋放後,其他因為需要獲得同一物件的鎖而無法正常執行的synchronized()
語句塊不會被自動喚醒,需要人工notify()
一下。實際上完全沒必要,這是我的誤解,只用使用wait()
被迫人工等待的執行緒需要被人工喚醒。(你係的鈴你解,系統系的鈴系統自己解!)
- 我對這個問題一直有一些誤解,以為被
Homework7
0. 寫在前面
迭代起來極具挑戰性(Physically & Mentally
)的一次作業:
-
Mentally
:-
DIY
電梯各項指標(速度/最大載客量/可達性) -
乘客需求自由,不再有起終點同座/層的限制
-
加上了這兩條要求後(主要是第二條),工作量幾乎可以說是對先前程式碼的全面重構。
-
Physically
:
沒錯,我就是盯著這樣一個支離破碎還漏液變色的電腦螢幕重構了10個小時。憑藉著超人的意志以及強大的視力完成了這次作業的我真的很佩服我自己。
P.S.
知識方法類的感想與收穫在前面兩次作業的總結中已經介紹的差不多了。HW7
將以介紹具體的設計思路與實現為主。
1. 迭代思路與難點
就是說因為乘客起點與終點並不一定在同座或同層,所以存在一個換乘的問題。如何進行換乘呢?
- 我採用的是從一開始便計算出換乘路徑的辦法。
我們先不關心路徑的演算法,先思考這樣一個問題:用什麼樣的資料結構來儲存路徑呢?
- 在這裡,我採用了一個類似索引查詢的方法利用一維陣列來儲存請求的路徑。
- 先對各樓座樓層元素進行編號:
A-1
: 0 --A-2
: 1 -- ... --E-10
: 49- \([\ spot = (block - 'A') * 10 + floor - 1\ ]\)
- 對於形如
i -> j -> k -> ... -> y -> z
的路徑,先初始化一個容量為50
,元素值均為-1
的一維陣列; - 令\(path[i]=j,\ path[j] = k,\ ...,\ path[y]=z,\ path[z]=z\)
- 這樣,只需傳入電梯當前所處位置,即可確定某任務下一移動位置。
- 先對各樓座樓層元素進行編號:
這裡我們可以注意到,由於path[]
陣列與PersonRequest
類現在是不可分割的,我們隨時都可能要同時獲得二者的資訊,或者依賴二者合作獲得我們想要的資料。
- 因此將它們封裝為一個新類,重新命名為
PersonRequestPath
(請原諒我蹩腳的命名)。
獲得路徑後,如何具體實現換乘操作呢?
- 首先,構造一個
getNextMove()
方法,傳入電梯當前位置,獲取下一步的移動方位(UP/DOWN/OVER/ROUND
) - 其次,構造一個單例執行緒類
TransferDispatcher
,負責與電梯執行緒進行資料互動(如SD圖中所描述)。電梯執行緒中請求確定getOut
後,判斷其下一步移動方位,若判定其需要換乘,則將其加入TransferList
(新的共享物件),與TransferDispatcher
進行資訊互動。兩個小細節:- 為了讓
TransferDispatcher
能成功定位請求的當前位置,在將請求加入中轉佇列前仍需一併傳入當前位置資訊。新建TransferInfo
將PRP
與curSpot
封裝在一起。 -
TransferList
作為新的共享物件也應構造為執行緒安全類。
- 為了讓
-
P.S.
原本電梯執行緒確定getOut
名單的方式是比較當前位置與請求終點位置是否相同。而現在由於換乘請求的加入,我們在判斷是否getOut
的方式需要更加靈活一點,只觀察下一步的移動方位,如果與當前電梯功能相符(Vertical -- UP/DOWN
,Horizontal -- ROUND
)則仍留在電梯中;否則(STAY/不相符
)下車。
換乘問題解決了。回到開始的那個問題:如何計算換乘路徑呢?
- 設計之初考慮過使用
Dijkstra
演算法,但是可行性不強。因為我們希望換乘次數儘量要少,而使用Dijkstra
演算法可能會出現七扭八歪的鬼才路徑,這是我們不希望看到的。 - 最終,我採用了有些智障的遍歷演算法。只在起點終點兩層之間尋找是否存在橫向可達的電梯,如果存在便選擇這條始終向目標靠近的路徑,否則,暴力一層中轉。
這樣,請求多樣化的迭代就基本完成了。
- 還有一點需要注意,就是在構造橫向可達性的臨接矩陣時,一定要將所有聯通可達樓層之間的邊全部置一。表述的不是很清晰,舉個栗子:
A-1/C-1/D-1/E-1
---> \(4 *3 =12\) 個元素均為1。
最後的最後,由於換乘的存在,我們遭遇到了一個棘手的問題:
即使此時輸入已經關閉,某電梯為空,也不代表該電梯執行緒一定結束。因為可能在其他電梯內的某乘客一會兒可能會乘坐該電梯中轉。解決方案:構造一個Counter
計數器,每輸入一個新請求,呼叫incrementTotalRequests()
,記錄請求總數;每完成一個請求,呼叫incrementFinishedRequests()
,記錄完成請求數。在輸入關閉後,判斷兩個static int
變數是否相等,若相等,則各執行緒setEnd()
;若不等,則wait()
。這樣在避免反覆輪詢的同時,可以起到判斷程式是否終止的作用。
2. 架構模式(類圖/SD圖)
P.S.
由於本次結構較為複雜,略去末尾setEnd()
的資訊傳遞。
3. Bug修復
忽略了一個小問題:
由於同層橫向電梯的規格可能有所不同,導致橫向換乘乘客的需求並非每臺電梯均可滿足。在設計載客邏輯時忽略了這一問題,導致某些乘客不小心坐上了錯誤的電梯,而無法在到達目的地時成功下車。
主要在以下三處進行了修改:
-
主任務的判斷:電梯在選擇主任務時必須保證有能力完後才能該任務,為了使等候佇列分配給電梯正確的主任務,需要在
getMainMissionFromFloorWaitQueue()
函式中傳入電梯的access
資訊。P.S.
由於對主任務選擇邏輯進行修正後,即使在等候佇列不為空的前提下,主任務獲取仍有可能失敗,因此在函式返回值為null
的情況下,需新增wait()
語句進行異常處理。 -
獲取
getInList
:在獲得可進入乘客名單時,也需要在getRequestInCurBlock
函式中傳入電梯的access
資訊。 -
增加一個新函式:以上兩處改動中,在判斷乘客是否可以乘坐當前電梯時,均需要獲取乘客在橫向電梯的換乘終點。因此,新增
getNextBlock()
函式獲取這一資訊。
Summary
-
本單元作業可以暴露出我對上一單元相關知識仍然沒有熟練掌握的問題,比如多型:在第三次迭代後,我的
WaitQueue
類方法中出現了多個形如getMainMissionFromCurBlock
,getMainMissionFromCurFloor
,getgetRequestOnCurFloor
,getRequestInCurBlock
本可以用Overload
(過載)方式實現的程式碼,而這些問題是我在完成作業後才意識到的。 -
關於工廠模式,在第二次迭代要求中增加橫向電梯時,我就在想使用
Elevator
介面運用工廠模式建模是不是會更規範一些。但是幾經斟酌我還是沒有這麼幹,仍然沒有體會到工廠模式在實際應用中的實用性。
------------恢復內容結束------------