天天看點

多線程程式設計初探——OO第二單元作業回顧

一、作業設計政策

1)執行FAFS政策的單部電梯

​ 由于對多線程不是很了解,于是采用了理論課上介紹的生産者消費者模型作為設計模闆(也是很多同學一開始的做法):将請求隊列作為共享對象(托盤),名為Input_handler的類處理輸入的請求并将請求加入到請求隊列(相當于生産者),排程器類則負責從請求隊列中取請求并直接執行(相當于消費者)。

​ 同步控制方面,由于共享對象queue中的請求隊列是可變對象,是以可能有線程不安全的問題,簡單的解決方法是将能夠修改該可變對象的兩個方法add/pop設定同步控制synchronized即可,這樣保證兩個線程不會同時通路修改共享對象。

​ 在本次作業中我選擇了好寫的傻瓜排程,線程間的協同也僅僅是排程器和輸入處理各自和共享對象互動,總體設計難度不大。

2)執行捎帶政策的單部電梯

​ 此次作業需要考慮電梯的性能,也就意味着需要調整原有的設計架構,加入捎帶功能。我考慮過兩種捎帶政策,差別在于“電梯知道多少資訊”:在第一種架構中,電梯隻需要知道接下來向哪個方向運動,目前層是否開門,不需要知道電梯上的乘客資訊;而在第二種架構中,電梯知道乘客的資訊,可以自主決定向哪運動,向哪開門。

​ 明顯可以看出,第二種架構相當于賦予了電梯一定的自主決定權,而第一種情況下的電梯則隻具備執行請求功能。第一種設計的電梯内聚性更低(代價是電梯控制器的複雜度明顯上升了),但耦合度較高,每次運動、開門、上下乘客時都要互動。第二種設計的電梯内聚度比較高,但是耦合度低,電梯隻需要從請求隊列中取請求進行執行,每運動一層自行決定是否上下乘客。

​ 其實總的任務是一樣的,不同的設計隻是在如何分解大的任務方面存在分歧,上面的第一種設計相當于在第二種設計上把“請求”更細緻地解析為了每一層的移動政策,在實際運用中更安全同時在某些情形下可以實作更為精細的控制,但第二種設計實作起來無疑更加簡單可靠。

​ 在沃艾思(作業系統)課程壓力較大的情況下我最後還是求穩選擇了第二種架構:由于隻有單部電梯不涉及請求配置設定,砍掉了排程器隻保留請求隊列,在請求隊列中編寫兩個請求配置設定方法(主請求和捎帶請求);電梯在乘客隊列為空時,向請求隊列申請主請求(沒有主請求則wait),拿到主請求開始工作,每運作一層後從隊列中獲得一個捎帶請求,如果捎帶請求不為空(有人可以捎帶)或者有乘客要下電梯則開門,開門後先下乘客(到達目的地的乘客)後上捎帶乘客(while 捎帶請求不為null則get捎帶請求);捎帶請求并入乘客隊列(副請求),在執行完主請求時選擇一個最近的副請求更新為主請求;所有請求執行完畢且輸入關閉時電梯下班(退出線程)。

​ 關于捎帶政策:官方推薦僅在能上且目的地和電梯運作方向一緻時才捎帶,但實際上有一種卑鄙的外鄉人換乘政策更簡單,且效果也不差(由于這部電梯可以無限載重),即在能上時就把人帶上,每次執行完一個請求後都切換到一個目的地距離目前樓層最近的副請求,這樣不僅可以在電梯運作時間上性能優于ALS捎帶政策,同時還節省了開門的時間。

​ 線程協作關系基本上還是生産者-消費者模式,托盤增加了一個獲得捎帶請求的方法(花式消費)。

​ 線程同步控制方面,将增加請求、擷取主請求、擷取捎帶請求這三個方法設為synchronized即可。

3)多電梯協同工作

​ 這次作業主要的變化是電梯數量和電梯停靠樓層的變化(載重問題隻需要在上次作業的基礎上根據電梯目前人數限制捎帶即可),前者使得工作的線程數增多了,後者則使得單個乘客請求的執行可能需要電梯間合作執行。具體到程式設計實作,需要有政策地拆分某些請求,并保證拆分掉的請求按一定的次序得到執行(先要執行完前一個子請求才能執行後一個)。此外,對于某些不需要拆分的請求,由于三部電梯運作速度不一緻,排程政策也不是唯一的。

​ 首先是拆分請求,所謂拆分實際上就是把單部電梯無力完成的任務拆分為兩部電梯協作完成的任務,第一部電梯接到乘客将其送到換乘樓層(兩部電梯的公共樓層),第二部電梯從換乘樓層接到乘客并把乘客送到目的地。不難知道,最簡單的拆分政策是把換乘樓層設定為三部電梯的公共樓層1和15,這樣所有請求都能被拆分後由單部電梯執行;更優的政策則是考慮選擇能使使用者乘坐電梯時間最小化的換乘樓層;當然,可以在短時間内做一些優化,例如考慮合作者目前所在的樓層和合作者搭載的乘客(合作者有自己的運作計劃,且前往換乘樓層需要時間),但是這無疑使問題變得相當複雜,且隻對極少情況下的性能有提升(考慮到三部電梯神奇的樓層分布使得請求拆分的情況較少),是以我僅僅考慮了使用者乘坐電梯的總時間。

​ 其次是保證請求的執行順序,這裡我類比了一下work-thread模式:我的請求隊列相當于channel,電梯相當于worker,不同的是這裡的worker執行的工作可能是多工序的,worker完成自己可以完成的那道工序之後會通知channel “這一步我搞定了”。而具體到我的實作,就是把拆分後的請求(兩道工序)打包起來送給一部電梯,電梯完成第一個子請求(第一道工序)後将第二個子請求加入請求隊列(通知channel第一道工序已經完成)。這樣便能保證請求按順序執行。至于兩個子請求如何打包傳入,我建立了一個request類用來存這兩個子請求(第二個子請求可以為空,表示該請求沒有被拆分)。

​ 那麼剩下的問題就是:電梯如何得到請求?既可以是電梯向排程器“搶”請求,也可以是排程器根據電梯的狀态将請求配置設定給電梯。同樣地,因為我不希望上調排程器的複雜度,是以我還是繼承了上次作業的“電梯搶請求”的方式,排程器隻需要負責配置設定和檢查有無捎帶乘客即可。

​ 由于整體架構和上次作業相同,線程協作和同步控制的方式總的來說和之前一樣,差別在于我修改了請求隊列(排程器)的add方法,在每次加入請求時判斷是否需要拆分請求(如果無法由單部電梯執行則需要拆分)。

二、基于度量分析程式結構

1)作業程式結構分析

a.hw5

可是看他的架構平平無奇

方法複雜度:

多線程程式設計初探——OO第二單元作業回顧

類複雜度:

多線程程式設計初探——OO第二單元作業回顧

代碼總規模:

多線程程式設計初探——OO第二單元作業回顧

類圖:

多線程程式設計初探——OO第二單元作業回顧

線程協作關系:

多線程程式設計初探——OO第二單元作業回顧
b.hw6

方法複雜度:

可見配置設定捎帶請求的方法相對複雜。

多線程程式設計初探——OO第二單元作業回顧

類複雜度:

從此次作業開始電梯類變得複雜。

多線程程式設計初探——OO第二單元作業回顧

代碼總規模:

多線程程式設計初探——OO第二單元作業回顧

類圖:

可以看出整體的架構和第五次作業一緻,類的内部屬性方法進行了擴充。

多線程程式設計初探——OO第二單元作業回顧

線程協作關系:

多線程程式設計初探——OO第二單元作業回顧
c.hw7

類複雜度:

多線程程式設計初探——OO第二單元作業回顧

顯然,由于需要自行判斷運作方向和開關門政策,電梯類的複雜度較高。

方法複雜度:

多線程程式設計初探——OO第二單元作業回顧

可見請求隊列(排程器)中拆分請求和配置設定主請求的方法複雜度較高。

總代碼規模:

多線程程式設計初探——OO第二單元作業回顧

類圖:

整體結構依然和前兩次作業一緻,内部的屬性方法進一步複雜化,引入了Request和Type兩個類,前者用于容納一個請求拆分得到的兩個子請求,後者用于規範化電梯的屬性。

多線程程式設計初探——OO第二單元作業回顧

線程協作關系:

多線程程式設計初探——OO第二單元作業回顧

2)優缺點評論

​ 三次作業采用的架構基本一緻,優點在于沿襲了設計模式,線程安全得以保證;缺點在于把相當一部分運作政策封裝進了電梯,排程器的作用僅剩下“提供請求讓電梯來拿”,無法根據電梯目前的樓層和電梯的運作速度做針對性的配置設定政策,因而性能并不頂尖。

3)SOLID原則

a.SRP單一責任原則

​ 較好實作——電梯隻負責運作和在停靠樓層與排程器互動(确定是否開門),排程器負責拆分和配置設定請求,内置請求隊列;輸入處理負責向排程器的請求隊列中加入新的請求。

​ 如果不增加新的類,則所有的工作将由這幾個類分擔,想要減少一個類的責任,勢必增加另一個類的,如不新增類(例如電梯控制器、樓層等等)很難更好地符合單一責任原則。

b.OCP開放封閉原則

​ 實作較好,三次作業架構始終不變,第六次作業擴充了捎帶請求方法,第七次作業擴充了拆分請求的方法。

c.LSP裡氏替換原則

​ 不适用,并未使用繼承,第三次作業用可變參數的方法實作了多電梯。

d.ISP接口分離原則

​ 不适用,程式除了線程使用了Thread接口,沒有使用其他接口。

e.DIP依賴倒置原則

​ 雖然我沒有使用抽象接口,但是的确對程式設計進行了抽象而非針對實作進行程式設計,可以說實作地較好。

三、程式bug

1)公測中出現的bug

​ 本次公測(至今最灰暗的一次)中我的程式出現了兩個邏輯上的錯誤,并且是小規模手造資料難以暴露的bug,加上我沒有自己在課下進行自動化測試,中測樣例又比較仁慈,是以強測分數十分喜人(如果分數評論标準和田徑比賽一樣的話)。

​ 第一個錯誤是電梯arrive樓層之後和排程器的互動,應當先确定目前樓層能夠停靠之後再進行捎帶和下乘客的互動,但是我将這兩者(互動和确定停靠)的順序寫反了,導緻會出現如下情況:電梯拿到一個捎帶請求,發現停靠不了,然後請求就沒有加入電梯,沒有加入電梯……這導緻在某些情況下乘客和消失了一樣(應該是掉入電梯井了?),程式無法正确執行。

​ 第二個錯誤是電梯是在乘客上電梯之後才增加載重,而不是在相應請求之後就增加載重,于是一開始的主請求在被響應之後,電梯載重為0,然後在去接主請求的路上已經裝滿了捎帶請求,這樣等到電梯終于到了主請求上電梯的樓層時,主請求如果直接進入,那麼違反了載重限制;如果不上電梯,那麼電梯的主請求沒有得到執行。兩種情況都會出錯。

​ 這兩個

bug

一個發生

elevator

類中負責電梯互動的方法

interact

裡,另一個是在共享對象請求隊列的

mainreq

方法中。都與線程安全無關,是電梯運作邏輯上的錯誤。

2)bug與設計結構的相關性

​ 本次作業會出現這兩個bug,和我的設計結構是有一定關系的。

​ 第一個bug的出現是因為在繼承上次設計的基礎上對于“判斷目前樓層能否停靠”判斷的位置不恰當,放在了拿到請求後(本應該在拿到請求前判斷),導緻可能出現請求未被執行的情況。

​ 第二個bug——由于我采用的是“主請求+捎帶”的響應請求邏輯,是以和現實生活中電梯的邏輯是有一定差異的:響應了主請求,電梯事實上就做出了保證一定要接到主請求,此時即應留出主請求的空位確定其一定能搭乘電梯。但是我在考慮人數限制的時候,依然是按照現實生活中的電梯運作邏輯來控制的:上人載重+1,下人載重-1,看似沒問題,實際可能出現主請求進入滿載電梯的情況。

### 四、互測政策

​ 本單元前兩次作業放了幾個空刀,沒有發現同屋同學的bug,第三次作業沒有進入互測,是以總的來說互測經曆沒有太多東西值得總結。

五、學習心得

1)如何保證線程安全

​ 在保證線程安全方面,我認為應該重點關注共享對象的通路控制,同時對可能死鎖的情況予以提前考慮。當然在三次作業中,synchronized方法鎖基本上就能解決所有問題,且性能損失可以接受,不太需要用到原子操作、對象鎖等等;同時作業中隻有一個共享資源、不太可能出現死鎖,選取合适的設計模式加以改造,可以比較好地處理作業中的線程安全問題。

2)設計原則

​ 設計原則方面,我認為應當參考SOLID原則進行程式設計,這樣可以保證程式的可靠性且易于擴充。同時我們應當善用不同的設計模式,提高程式效率、保證程式安全性。

轉載于:https://www.cnblogs.com/why34/p/10763545.html

繼續閱讀