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