編寫正確的並行程式是一件困難的事情,往往偵錯過程中發生很多不確定的事情,這時需要對理論知識的一個認知,能夠準確的追蹤問題。
CPU,記憶體,I/O裝置不斷迭代,這三者速度存在差異,CPU和記憶體的速度差異可以形象的描述:CPU速度最快,記憶體次之,I/O裝置更次之。
為了合理利用CPU的高效能,平衡這三者的速度差異,電腦架構、作業系統、編譯程式都做出了貢獻,主要體現為:
快取導致的可見性問題
在單核時代,所有的執行緒都是在一顆 CPU 上執行,CPU 快取與記憶體的資料一致性容易解決,所有都是序列。
一個執行緒對共用變數的修改,另一個執行緒能夠立刻看到,我們稱為可見性。
多核時代,每顆 CPU 都有自己的快取,這時 CPU 快取與記憶體的資料一致性就沒那麼容易解決了,當多個執行緒在不同的 CPU 上執行時,這些執行緒操作的是不同的 CPU 快取。
執行緒切換帶來的原子性問題
由於 IO 太慢,早期的作業系統就發明了多程序,即便在單核的 CPU 上我們也可以一邊聽著歌,一邊寫 Bug,這個就是多程序的功勞。
作業系統允許某個程序執行一小段時間,例如 50 毫秒,過了 50 毫秒作業系統就會重新選擇一個程序來執行(我們稱為「工作切換」),這個 50 毫秒稱為「時間片」。
我們把一個或者多個操作在 CPU 執行的過程中不被中斷的特性稱為原子性。CPU 能保證的原子操作是 CPU 指令級別的,而不是高階語言的操作符,這是違揹我們直覺的地方。因此,很多時候我們需要在高階語言層面保證操作的原子性。
我們把一個或者多個操作在 CPU 執行的過程中不被中斷的特性稱為原子性。
「原子性」的本質是什麼?其實不是不可分割,不可分割只是外在表現,其本質是多個資源間有一致性的要求,操作的中間狀態對外不可見。
編譯優化帶來的有序性問題
那並行程式設計裡還有沒有其他有違直覺容易導致詭異 Bug 的技術呢?有的,就是有序性。顧名思義,有序性指的是程式按照程式碼的先後順序執行。編譯器為了優化效能,有時候會改變程式中語句的先後順序,例如程式中:「a=6;b=7;」編譯器優化後可能變成「b=7;a=6;」,在這個例子中,編譯器調整了語句的順序,但是不影響程式的最終結果。不過有時候編譯器及直譯器的優化可能導致意想不到的 Bug。
在介紹可見性、原子性、有序性的時候,特意提到快取導致的可見性問題,執行緒切換帶來的原子性問題,編譯優化帶來的有序性問題,其實快取、執行緒、編譯優化的目的和我們寫並行程式的目的是相同的,都是提高程式效能。
參考
《Java並行程式設計實戰》
公眾號
名稱:巨量資料計算
微訊號:bigdata_limeng