(十一)併發執行的必要性(同步),產生的問題,原子操作,爲什麼引入鎖機制,麪包購買的類比

視頻對應內容:
9.1 背景知識
9.2 一些概念part1
9.3 一些概念part2
9.4 一些概念part3

一、同步互斥的背景

到目前爲止學習了:

  • 多道程序設計(multi-programming):現代操作系統的重要特性;
  • 並行很有用,因爲:
    • 多個併發實體:CPU(s),I/O,…,用戶,…
  • 進程/線程:操作系統抽象出來用於支付多道程序設計;
  • CPU調度:實現多道程序設計的機制;
  • 調度算法:不同的策略。

將要學習:

  • 協同多道程序設計和併發問題。

進程間不獨立存在風險

獨立的線程:

  • 不和其他線程共享資源或狀態;
  • 確定性:輸入狀態決定結果;
  • 可重現:能夠重視啓示條件,I/O;
  • 調度順序不重要。

合併線程:

  • 在多個線程中共享狀態;
  • 不確定性;
  • 不可重現。

不確定性和不可重現意味着bug可能是間歇性發生的。

進程間爲什麼合作?

進程/線程,計算機/設備需要合作。

  • 優點1:共享資源
    • 一臺電腦,多個用戶;
    • 一個銀行存款餘額,多臺ATM機;
    • 嵌入式系統(機器人控制,手臂和手的協調)。
  • 優點2:加速
    • I/O操作和計算可以重疊;
    • 多處理器:將程序分成多個部分並執行。
  • 優點3:模塊化
    • 將大程序分解成小程序:以編譯爲例,gcc會調用cpp,cc1,cc2,as,Id;
    • 使系統易於擴展。

例:併發執行產生問題

程序可以調用函數fork()來創建一個新的進程

  • 操作系統需要分配一個新的並且唯一的進程ID;
  • 因此在內核中,這個系統調用會運行
new_pid = next_pid++;
// next_pid爲共享的全局變量,賦id用
  • 將上述類c語言翻譯成機器指令
LOAD next_pid Reg1   // 將new_next值賦給寄存器1
STORE Reg1 new_pid   // 將寄存器1值存到new_pid
INC Reg1             // 寄存器1值加一
STORE Reg1 next_pid  // 將寄存器1值存到next_pid

假設兩個進程併發執行:

  • 如果next_pid 等於100,那麼其中一個進程得到的ID應該是100,另一個進程的ID應該是101,next_pid應該增加到102。

但是,併發進程存在如下圖的問題。
在這裏插入圖片描述

進程1執行一半,發生了上下文切換。
程序可以在上面4條語句中的任意一句進行調度,這樣就會出現不同的結果。(不好)

最終進程1、進程2的PID都是100。可能在任何語句間產生上下文切換,導致結果出現不確定性並不可重複。

因此希望:

  • 無論多個線程的指令序列怎樣交替執行,程序都必須正常工作(希望和預期的結果一樣):
    • 多線程程序具有不確定性和不可重現的特點;
    • 不經過專門設計,調試難度很高;
  • 不確定性要求並行程序的正確性:
    • 先思考清楚問題,把程序的行爲設計清楚;
    • 切忌急於着手編寫代碼,碰到問題再調試。

二、Race Condition(競態條件)

出現競態條件,就說明會出現不確定性。

系統缺陷:結果依賴於併發執行或者事件的順序/時間。

  • 不確定性
  • 不可重現

怎樣避免競態?

  • 讓指令不被打斷

三、Atomic Operation(原子操作)

原子操作: 不可被打斷的操作。

原子操作是指一次不存在任何中斷或者失敗的執行。

  • 該執行成功結束;
  • 或者根本沒有執行;
  • 並且不應該發現任何部分執行的狀態。

實際上操作往往不是原子的。

  • 有些看上去是原子操作,實際上不是;
  • 連x++這樣的簡單語句,實際上是由3種指令構成的;
  • 有時候甚至連單條機器指令都不是原子的
    • Pipeline, super-scalar, out-of-order, page fault

內存讀取是原子的,但未必結果確定

在這裏插入圖片描述
如上圖,在c語言層次上,保證原子操作,但是在程序設計上出現了問題。

第一種情況:
可能調度器先選擇了線程A,先打印A;
第二種情況:
可能調度器先選擇了線程B,先打印B;
第三種情況:
當線程1執行到 i=i+1時 ,調度器切換到 i=i-1,相當於 i 的值沒有產生變化,就會出現線程A在while循環裏一直循環,線程2一直在它的while循環裏一直循環,從而出現誰也不可能贏的情況。

我們需要一種機制,讓 或者A贏,或者B贏。

四、由此引出相關基本概念

在這裏插入圖片描述

Critical section(臨界區)

臨界區 是指進程中一段需要訪問共享資源(比如上面例子中的 全局變量 i ),並且當另一個進程處於相應代碼區域時,不會被執行的代碼區域。

Mutual exclusion(互斥)

當一個進程處於臨界區並訪問共享資源時,沒有其他進程會處於臨界區並且訪問任何相同的共同資源。

Dead lock(死鎖)

兩個或以上的進程,在相互等待完成特定任務,而最終沒法將自身任務進行下去。

Starvation(飢餓)

一個可執行的進程,被調度器持續忽略,以至於雖然處於可執行狀態卻不被執行。(無限期等待)

五、操作系統調度的生活類比(購買麪包)

在這裏插入圖片描述
如上圖,出現了兩次“買麪包”的操作。(買了2份麪包)

什麼是“麪包太多”問題的正確性質?

  • 最多有一個人去買麪包;
  • 如果需要,纔去買麪包。

在冰箱上設置一個鎖和鑰匙(lock&key)

  • 去買麪包之前鎖住冰箱並且拿走鑰匙;
  • 修復了“太多”的問題:要是有人想要果汁怎麼辦?
  • 可以改變“鎖(lock)”的含義;
  • “鎖(lock)”包含“等待(waiting)”。

Lock(鎖)

在門、抽屜等物體上加上保護性裝置,使得外人無法訪問物體內的東西,只能等待解鎖後才能訪問。

Unlock(解鎖)

打開保護性裝置,使得可以訪問之前被保護的物體類的東西。

Deadlock(死鎖)

A拿到鎖1,B拿到鎖2,A想繼續拿到鎖2後再繼續執行,B想繼續拿到鎖1後再繼續執行。導致A和B誰也無法繼續執行。

解決麪包購買辦法

在這裏插入圖片描述
如上圖,增加一種“標籤”,當成一種“鎖”,但是依然會產生問題。
在這裏插入圖片描述
執行順序如上圖,簡單實用note機制無法解決問題,甚至會使問題更糟。

爲便籤加標籤怎麼樣?表示一下誰放的標籤,如下圖。
在這裏插入圖片描述
但是如上圖,有可能出現誰都不去買麪包的情況。

在這裏插入圖片描述
可以提出如上的機制,保證程序的正常執行。

還有更好的解決方案麼

之前方案獲得的啓示

上述方法太複雜,A、B代碼不同,A等待時實際在消耗CPU的時間(叫做“忙等待busy-waiting”)。

上述方案爲每個線程保護了一段“臨界區(critical-section)”,代碼爲

if (nobread) {
	buy bread;
}

如果有一個進程已經處於臨界區,則其他進程不能進入臨界區。
互斥就是確保只有一個進程在臨界區。

假設我們有一些鎖的實現

  • Lock.Acquire() 在鎖被釋放前一直等待,然後獲得鎖;
  • Lock.Release() 解鎖並喚醒任何等待中的進程;
  • 這些一定是原子操作:如果兩個線程都在等待同一個鎖,並且同時發現鎖被釋放了,那麼只有一個能夠獲得鎖。
  • 由此,麪包問題得到解決:
breadlock.Acquire();  // 進入臨界區
if (nobread) {
	buy bread;
}
breadlock.Release();  // 退出臨界區

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章