Clean Code 無瑕的程式碼 第13章 平行化
?? ? ? 第十三章的主題是平行化,平行化又是一門大學問可以寫好幾本書,作者坦言本章只有提到平行化常見粗淺的部分,可做為讀者入門的參考。
為什麼要平行化?
平行化 將「做什麼」與「何時做」分解開來
平行化的優(yōu)點
改善應用程式的產能
縮短整體回應時間
書中提到的範例有類似I/O讀取的情況最適合平行化,單一執(zhí)行緒一定會拖長反應時間。
迷思及誤解
平行化總可以改善效能?
部分情況平行化才能改善效能,例如有很多等待時間、可多執(zhí)行緒或多重處理器(需要硬體配合)。
平行化不需要修改原有的設計?
平行化會在系統(tǒng)的結構上產生巨大改變。
當利用Web 或EJB容器來處理平行化時,了解平行化所導致的問題變得不是那麼重要?
平行化會出現新的問題,有可能會出現平行化更新與死結的問題。
平行化注意事項
平行化有額外的負擔,包含程式的效能,要撰寫額外的程式碼
正確的平行化是複雜的
平行化程式的錯誤不容易重複出現
平行化會要求對於設計策略進行根本性的修改
挑戰(zhàn)
看範例可知內容,多執(zhí)行緒程式,有可能會出現不同的答案。
平行化的防禦原則
單一職責原則
要修改方法、類別、元件只能有一個理由。
建議:保持平行化相關程式與其他程式有清楚的劃分。
程式要分為平行化部分與非平行化部分。
推論:限制資料的視野
兩個執(zhí)行緒修改共享物件的同一個欄位,就會互相干擾。
可用關鍵字 synchronized (同步化) 關鍵字保護共享物件的臨界區(qū)域(critical section)程式碼。
越多更多的地方可以更新共享資料,就越容易產生下列狀況:
忘記保護臨界區(qū)域
確認有效保護,會花上重複工夫
除錯困難
建議:資料封裝,限制共享資料的存取次數。
推論:使用資料的副本
複製就可以避免出現共享的情況,可以節(jié)省鎖定的時間,會付出額外物件與需要回收垃圾的代價。
推論:執(zhí)行緒應盡可能地獨立運行
儘量讓每個執(zhí)行緒有自己的世界,與其他執(zhí)行緒不共享任何資料。
建議:試著將資料劃分成可以讓獨立執(zhí)行緒操作的獨立子集合。
了解你的函數庫
Java5提供許多平行化開發(fā)的改善,應注意下列幾點:
使用函式庫所提供的安全執(zhí)行緒集合(thread-safe collections)
使用executor框架來執(zhí)行不相關的工作(?)
盡可能使用非鎖定(nonblocking)的解法
有幾個函數庫類別並不提供安全執(zhí)行緒
安全執(zhí)行緒集合
可參考這篇文章
Day6 執(zhí)行緒安全的介紹
https://ithelp.ithome.com.tw/articles/10203335
ReentrantLock
https://www.jianshu.com/p/155260c8af6c
一個可以被A方法中取得,並在B方法中釋放的鎖
一個執(zhí)行緒執(zhí)行,其他執(zhí)行緒等待。
ReentrantLock的使用
Semaphore
一個具有計數功能的鎖。
設定一定的執(zhí)行許可証數量,其他執(zhí)行緒爭搶這些許可證。
CountDownLatch
釋放所有等待這個鎖的執(zhí)行緒之前,會先等待指定數量的事件,使得所有執(zhí)行緒都有公平機會在同時間啟動
一個執(zhí)行緒執(zhí)行,其他執(zhí)行緒等待。有個計數機制判定執(zhí)行緒是否可以執(zhí)行。
了解你的執(zhí)行模型
平行化相關重要名詞如下
有限資源(Bound Resources)
資源有限制,例如資料庫的連線數量與讀寫緩衝區(qū)的大小。
互斥(Mutual Exclusion)
同一時間內只有一個執(zhí)行緒可以存取共享的資源。
飢餓(Starvatiion)
一個執(zhí)行緒永遠或很長的時間無法執(zhí)行。
死結(Deadlock)
多個執(zhí)行緒互相等待對方的回應或資源,導致互相等待無法執(zhí)行的情況。
活結(Livelock)
多個執(zhí)行緒都想動卻互相卡住無法執(zhí)行。
這個網頁有張圖很明顯表現出何謂活結
What's the difference between deadlock and livelock?
https://stackoverflow.com/questions/6155951/whats-the-difference-between-deadlock-and-livelock/28715920
平行化程式設計的執(zhí)行模組
生產者-消費者
生產者消費者問題
https://zh.wikipedia.org/wiki/%E7%94%9F%E4%BA%A7%E8%80%85%E6%B6%88%E8%B4%B9%E8%80%85%E9%97%AE%E9%A2%98
生產者? 執(zhí)行緒建立工作將工作放在佇列
消費者? 執(zhí)行緒從佇列中取出這些工作
佇列是有限資源,生產者必須等到佇列有空間才能寫入,消費者必須等到佇列裡有東西才能取出消費。
生產者與消費者之間以信號溝通。
讀取者-寫入者
Readers–writers problem
https://en.wikipedia.org/wiki/Readers%E2%80%93writers_problem
一個共享資源主要當做讀取者讀取的資訊來源,也有會被寫入者更新的情況,資料庫讀寫是其中一個例子。
加強讀取產能會出現寫入執(zhí)行緒飢餓,允許資料更新又會影響產能。
關鍵點在於如何平衡讀取與寫入的需求,能夠提供合理的產能又能夠避免飢餓的情況。
哲學家用餐
哲學家就餐問題
https://zh.wikipedia.org/wiki/%E5%93%B2%E5%AD%A6%E5%AE%B6%E5%B0%B1%E9%A4%90%E9%97%AE%E9%A2%98
執(zhí)行緒互相爭奪資源的情況,有可能會出現死結、活結、產能以及效能下降的問題。
當心同步方法之間的相依性
??????????
保持同步區(qū)塊的簡短
synchronized 關鍵字可以製造一個鎖定,程式碼區(qū)塊由同一個鎖定防護,確保一次只有單一執(zhí)行緒會執(zhí)行區(qū)塊內的程式。
保護區(qū)域過大會導致資源競爭增加,而降低了效能。
建議:同步區(qū)塊越簡短越好
撰寫正確的關閉(Shut-Down)程式碼是困難的
正確停止很難達成,因為經常遇到死結問題。有的執(zhí)行緒會持續(xù)等待一個永遠不會來的信號(有可能其中一個執(zhí)行緒掛掉了)。
建議:早點思考關於關閉的問題。
測試執(zhí)行緒程式碼
建議:曾經出現錯誤就代表一定有錯誤,不可以忽略錯誤。
測試執(zhí)行緒的建議
將偽造的失敗看作是潛在的執(zhí)行緒問題
平行化程式執(zhí)行的時候有可能會出現偶發(fā)錯誤,一定是程式有問題!不可以當做是假的。
建議:不要把系統(tǒng)錯誤當做偶發(fā)事件。
先讓你的非執(zhí)行緒程式碼能順利運行
先考慮單一執(zhí)行緒的情況,做到單一執(zhí)行緒正確,再考慮多執(zhí)行緒的情況。
讓你的執(zhí)行緒是可隨插即用的
?????????????
大意是測試程式碼可以隨意添加或刪減,可以模擬不同的情況。
讓你的執(zhí)行緒程式碼是可調校的
可以調整程式的環(huán)境參數,例如執(zhí)行緒的數量。
執(zhí)行比處理器數量還要多的執(zhí)行緒
要測試工作交換(task swapping)的情況,執(zhí)行緒數量要比處理器或核心數量還要多。
在不同的平臺上運行
平行化程式要在不同的作業(yè)系統(tǒng)上進行測試。
調整你的程式碼,使之試圖產生失敗或強制產生失敗
如何抓到偶爾會發(fā)生的錯誤?在程式增加測試函數影響執(zhí)行順序,可以增加錯誤發(fā)生的機率。有問題的程式碼能夠越早發(fā)現是比較好的情況。
有兩個替程式碼加工的情況手動撰寫與自動化
手動撰寫
手動撰寫測試程式會遇到一些問題
必須找到合適的地方插入測試程式
要放置合種測試函數
測試程式會影響程式執(zhí)行的速度
能不能找到錯誤要看運氣
自動化
透過 Aspect-Oriented Framework (剖面導向框架)、CGLIB 或 ASM 。
可以想像類別有兩種實作,第一種實作測試程式什麼都不做,第二種測試程式會出現影響力,製造各種測試條件。
總結
多執(zhí)行緒與共享資料設計讓平行化程式變困難。
程式要分成與執(zhí)行緒有關與執(zhí)行緒無關的部分,與執(zhí)行緒有關的程式要更加短小。
平行化可能發(fā)現的問題
多執(zhí)行緒操作共享資料
使用公共資源
邊界情況,例如關閉程式。
找出必須被鎖定的區(qū)域,不要鎖定過多的區(qū)域。避免從一個鎖定區(qū)域呼叫另一個鎖定區(qū)域。
了解哪些東西可以被共享??刂乒蚕砦锛臄盗?,縮小共享的視野。
將共享的資料提供給客戶端,而不是強迫客戶端來管理共享資料的狀態(tài)。
平行化程式有可能隨機性出現錯誤。
測試程式要有隨插即用的能力。
程式碼加工寫測試程式可增加找到錯誤程式的機率。可以手動撰寫或使用自動化科技工具。