1. 程式人生 > 其它 >模組四:程序和多執行緒: 練習題詳解

模組四:程序和多執行緒: 練習題詳解

技術標籤:Linux

請問這個程式執行後, 輸出結果 Hello World 會被列印幾次?

  • fork()
  • fork()
  • fork()
  • print("Hello World\n")

fork 的含義是複製一份當前程序的全部狀態

  • 第 1 個 fork 執行 1 次產生 1 個額外的程序。 第 2 個 fork,執行 2 次,產生 2 個額外的程序。第 3 個 fork 執行 4 次,產生 4 個額外的程序。
  • 所以執行 print 的程序一共是 8 個

如果考慮到 CPU 快取的存在,會對上面我們討論的演算法有什麼影響?

  • 對某個地址,和任意時刻,如果所有執行緒讀取值,得到的結果都一樣,是一種強一致性,我們稱為線性一致性(Sequencial Consistency),
  • 含義就是所有執行緒對這個地址中資料的歷史達成了一致,歷史沒有分差,有一條大家都能認可的主線,因此稱為線性一致。
  • 如果只有部分時刻所有執行緒的理解是一致的,那麼稱為弱一致性(Weak Consistency)。

那麼為什麼會有記憶體不一致問題呢? 這就是因為 CPU 快取的存在。

  • 如上圖所示:假設一開始 A=0,B=0。兩個不在同一個 CPU 核心執行的 Thread1、Thread2 分別執行上圖中的簡單程式。
  • 在 CPU 架構中,Thread1,Thread2 在不同核心,因此它們的 L1\L2 快取不共用, L3 快取共享。
  • 在這種情況下,如果 Thread1 發生了寫入 A=1,這個時候會按照 L1,L2,L3 的順序寫入快取,最後寫記憶體。
  • 而對於 Thread2 而言,在 Thread1 剛剛發生寫入時,如果去讀取 A 的值,就需要去記憶體中讀,這個時候 A=1 可能還沒有寫入記憶體。
  • 但是對於執行緒 1 來說,它只要發生了寫入 A=1,就可以從 L1 快取中讀取到這次寫入。
  • 所以線上程 1 寫入 A=1 的瞬間,執行緒 1 執行緒 2 無法對 A 的值達成一致,造成記憶體不一致。
  • 這個結果會導致 print 出來的 A 和 B 結果不確定,可能是 0 也可能是 1,取決於具體執行緒執行的時機。

考慮一個鎖變數,和 cas 上鎖操作,程式碼如下:

int lock = 0
void lock() {
  while(!cas(&lock, 0, 1)){
    // CPU降低功耗的指令
  }
}
  • 上述程式構成了一個簡單的自旋鎖(spin-lock)。如果考慮到記憶體一致性模型,執行緒 1 通過 cas 操作將 lock 從 0 置 1。
  • 這個操作會先發生線上程所在 CPU 的 L1 快取中。
  • cas 函式通過底層 CPU 指令保證了原子性,cas 執行完成之前,執行緒 2 的 cas 無法執行。
  • 當執行緒 1 開始臨界區的時候,假設這個時候執行緒 2 開始執行,嘗試獲取鎖。
  • 如果這個過程切換非常短暫,執行緒 2 可能會從記憶體中讀取 lock 的值(而這個值可能還沒有寫入,還在 Thread 所在 CPU 的 L1、L2 中),執行緒 2 可能也會通過 cas 拿到鎖。
  • 兩個執行緒同時進入了臨界區,造成競爭條件。
  • 這個時候,就需要強制讓執行緒 2的讀取指令一定等到寫入指令徹底完成之後再執行,避免使用 CPU 快取。Java 提供了一個 volatile 關鍵字實現這個能力,只需要這樣:
volatile int lock = 0;

就可以避免從讀取不到對lock的寫入問題。

樂觀鎖、區塊鏈:除了上鎖還有哪些併發控制方法?

  • 樂觀鎖、悲觀鎖都能夠實現避免競爭條件,實現資料的一致性。
  • 比如減少庫存的操作,無論是樂觀鎖、還是悲觀鎖都能保證最後庫存算對(一致性)。
  • 但是對於併發減庫存的各方來說,體驗是不一樣的。悲觀鎖要求各方排隊等待。 樂觀鎖,希望各方先各自進步。

所以進步耗時較長,合併耗時較短的應用,比較適合樂觀鎖。

  • 比如協同創作(寫文章、視訊編輯、寫程式等),協同編輯(比如共同點餐、修改購物車、共同編輯商品、分散式配置管理等),非常適合樂觀鎖
  • 因為這些操作需要較長的時間進步(比如寫文章要思考、配置管理可能會連續修改多個配置)。
  • 樂觀鎖可以讓多個協同方不急於合併自己的版本,可以先 focus 在進步上。

悲觀鎖適用在進步耗時較短的場景

  • 比如鎖庫存剛好是進步(一次庫存計算)耗時少的場景。
  • 這種場景使用樂觀鎖,不但沒有足夠的收益,同時還會導致各個等待方(執行緒、客戶端等)頻繁讀取庫存——而且還會面臨快取一致性的問題(類比記憶體一致性問題)。
  • 這種進步耗時短,頻繁同步的場景,可以考慮用悲觀鎖。類似的還有銀行的交易,訂單修改狀態等。
  • 再比如搶購邏輯,就不適合樂觀鎖。搶購邏輯使用樂觀鎖會導致大量執行緒頻繁讀取快取確認版本(類似 cas 自旋鎖),這種情況下,不如用佇列(悲觀鎖實現)。

綜上

  • 有一個誤區就是悲觀鎖對衝突持有悲觀態度,所以效能低;
  • 樂觀鎖,對衝突持有樂觀態度,鼓勵執行緒進步,因此效能高。
  • 這個不能一概而論,要看具體的場景。
  • 最後補充一下,悲觀鎖效能最高的一種實現就是阻塞佇列,你可以參考 Java 的 7 種繼承於 BlockingQueue 阻塞佇列型別。

還有哪些程序間通訊方法

  • 使用資料庫
  • 使用普通檔案
  • 還有一種是訊號,一個程序可以通過作業系統提供的訊號。舉個例子,假如想給某個程序(pid=9999)傳送一個 USR1 訊號,那麼可以用:
  • kill -s USR1 9999
    

    程序 9999 可以通過寫程式接收這個訊號。 上述過程除了用kill指令外,還可以呼叫作業系統 API 完成。

如果磁碟壞了,通常會是怎樣的情況

  • 磁碟如果徹底壞了,伺服器可能執行程式報錯,無法寫入,甚至宕機。
  • 這些情況非常容易發現。而比較不容易觀察的是壞道,壞道是磁碟上某個小區域資料無法讀寫了。
  • 有可能是硬損壞,就是物理損壞了,相當於永久損壞。也有可能是軟損壞,比如資料寫亂了。
  • 導致磁碟壞道的原因很多,比如電壓不穩、灰塵、磁碟質量等問題。

磁碟損壞之前,往往還伴隨效能整體的下降

  • 壞道也會導致讀寫錯誤。
  • 所以在出現問題前,通常是可以在監控系統中觀察到伺服器效能指標變化的。
  • 比如 CPU 使用量上升,I/O Wait 增多,相同併發量下響應速度變慢等。
  • 如果在工作中懷疑磁碟壞了,可以用下面這個命令檢查壞道:sudo badblocks -v /dev/sda5