今日得到
-
計算機科學領域的任何問題都可以通過增加一個間接的中間層來解決
-
並發:
Do not communicate by sharing memory; instead, share memory by communicate.
(不要以共享內存的方式來通信,相反,要通過通信來共享內存)
1. 進程
進程是系統進行資源分配和調度的一個獨立單位,程序段、數據段、PCB三部分組成了進程實體(進程映像),PCB是進程存在的唯一標准
1.1 進程的組織方式:
- 鏈接方式
- 按照進程狀態將PCB分為多個隊列,就緒隊列,阻塞隊列等
- 操作系統持有指向各個隊列的指針
- 索引方式
- 根據進程狀態的不同,建立幾張索引表
- 操作系統持有指向各個索引表的指針
1.2 進程的狀態
-
創建態: 操作系統為進程分配資源,初始化PCB
-
就緒態:運行資源等條件都滿足,存儲在就緒隊列中,等待CPU調度
-
運行態:CPU正在執行進程
-
阻塞態:等待某些條件滿足,等待消息回復,等待同步鎖,sleep等,阻塞隊列
-
終止態 :回收進程擁有的資源,撤銷PCB
1.3 進程的切換和調度
進程在操作系統內核程序臨界區中不能進行調度與切換
臨界資源:一個時間段內只允許一個進程使用資源,各進程需要互斥地訪問臨界資源
臨界區:訪問臨界資源的代碼
內核程序臨界區:訪問某種內核數據結構,如進程的就緒隊列(存儲各進程的PCB)
進程調度的方式:
- 非剝奪調度方式(非搶占方式),只允許進程主動放棄處理機,在運行過程中即便有更緊迫的任務到達,當前進程依然會繼續使用處理機,直到該進程終止或者主動要求進入阻塞態
- 剝奪調度方式(又稱搶占方式)當一個進程正在處理機上執行時,如果有一個優先級更高的進程需要處理機,則立即開中斷暫停正在執行的進程,將處理機飯呢陪給優先級高的那個進程
進程的切換與過程:進程的調度、切換是有代價的
- 對原來運行進程各種數據的保存
- 對新的進程各種數據恢復(程序計數器,程序狀態字,各種數據寄存器等處理機的現場)
進程調度算法的相關參數:
- CPU利用率:CPU忙碌時間/作業完成的總時間
- 系統吞吐量:單位時間內完成作業的數量
- 周轉時間:從作業被提交給系統開始,到作業完成為止的時間間隔 = 作業完成時間-作業提交時間
- 帶權周轉時間:(由於周轉時間相同的情況下,可能實際作業的運行時間不一樣,這樣就會給用戶帶來不一樣的感覺) 作業周轉時間/作業實際運行時間, 帶權周轉時間>=1, 越小越好
- 平均帶權周轉時間:各作業帶權周轉時間之和/作業數
- 等待時間
- 響應時間
調度算法:
算法思想,用於解決什么問題?
算法規則,用於作業(PCB作業)調度還是進程調度?
搶占式還是非搶占式的?
優缺點?是否會導致飢餓?
以下調度算法是適用於當前交互式操作系統
- 時間片輪轉(Round-Robin)
- 算法思想:公平地、輪流地為各個進程服務,讓每個進程在一定時間間隔內可以得到相應
- 算法規則:按照各進程到達就緒隊列的順序,輪流讓各個進程執行一個時間片(如100ms)。若進程未在一個時間片內執行完,則剝奪處理機,將進程重新放到就緒隊列隊尾重新排隊。
- 用於作業/進程調度:用於進程的調度(只有作業放入內存建立相應的進程后,才會被分配處理機時間片)
- 是否可搶占?若進程未能在規定時間片內完成,將被強行剝奪處理機使用權,由時鍾裝置發出時鍾中斷信號來通知CPU時間片到達
- 優缺點:適用於分時操作系統,由於高頻率的進程切換,因此有一定開銷;不區分任務的緊急程度
- 是否會導致飢餓? 不會
- 優先級調度算法
- 算法思想:隨着計算機的發展,特別是實時操作系統的出現,越來越多的應用場景需要根據任務的進程成都決定處理順序
- 算法規則:每個作業/進程有各自的優先級,調度時選擇優先級最高的作業/進程
- 用於作業/進程調度:即可用於作業調度(處於外存后備隊列中的作業調度進內存),也可用於進程調度(選擇就緒隊列中的進程,為其分配處理機),甚至I/O調度
- 是否可搶占? 具有可搶占版本,也有非搶占式的
- 優缺點:適用於實時操作系統,用優先級區分緊急程度,可靈活地調整對各種作業/及進程的偏好程度。缺點:若源源不斷地提供高優先級進程,則可能導致飢餓
- 是否會導致飢餓: 會
- 多級反饋隊列調度算法
-
算法思想:綜合FCFS、SJF(SPF)、時間片輪轉、優先級調度
-
算法規則:
- 1.設置多級就緒隊列,各級別隊列優先級從高到底,時間片從小到大
- 2.新進程到達時先進入第1級隊列,按照FCFS原則排隊等待被分配時間片,若用完時間片進程還未結束,則進程進入下一級隊列隊尾
- 3.只有第k級別隊列為空時,才會為k+1級對頭的進程分配時間片
-
用於作業/進程調度:用於進程調度
-
是否可搶占? 搶占式算法。在k級隊列的進程運行過程中,若更上級別的隊列(1-k-1級)中進入一個新進程,則由於新進程處於優先級高的隊列中,因此新進程會搶占處理機,原理運行的進程放回k級隊列隊尾。
-
優缺點:對各類型進程相對公平(FCFS的有點);每個新到達的進程都可以很快就得到相應(RR優點);短進程只用較少的時間就可完成(SPF)的有點;不必實現估計進程的運行時間;可靈活地調整對各類進程的偏好程度,比如CPU密集型進程、I/O密集型進程(拓展:可以將因I/O而阻塞的進程重新放回原隊列,這樣I/O型進程就可以保持較高優先級)
-
是否會導致飢餓: 會
-
-
1.4 進程間通信方式
- 管道
- FIFO有名管道
- 消息隊列
- 信號量
- 共享內存
- 套接字 Socket
進程間通信IPC (InterProcess Communication)
1.5 僵屍進程/孤兒進程/守護進程
- 孤兒進程:如果父進程先退出,子進程還沒退出,那么子進程將被托孤給
init
進程,這時子進程的父進程就是init
進程(1號進程) 孤兒進程無危害 - 僵屍進程:如果我們了解過linux進程狀態及轉換關系,我們應該知道進程這么多狀態中有一種狀態是僵屍狀態,就是進程終止后進入僵屍狀態(
zombie
),等待告知父進程自己終止,后才能完全消失.但是如果一個進程已經終止了,但是其父進程還沒有獲取其狀態,那么這個進程就稱之為僵屍進程.僵屍進程還會消耗一定的系統資源,並且還保留一些概要信息供父進程查詢子進程的狀態可以提供父進程想要的信息.一旦父進程得到想要的信息,僵屍進程就會結束. 一個進程使用fork
創建子進程,如果子進程退出,而父進程並沒有調用wait
和waitpi
獲取子進程的狀態信息,那么子進程的進程描述符仍然保存在系統中。這種進程稱之為僵屍進程。 - 守護進程: 守護進程就是在后台運行,不與任何終端關聯的進程,通常情況下守護進程在系統啟動時就在運行,它們以root用戶或者其他特殊用戶(apache和postfix)運行,並能處理一些系統級的任務.習慣上守護進程的名字通常以d結尾(sshd),但這些不是必須的
危害: 孤兒進程結束后會被 init 進程善后,並沒有危害,而僵屍進程則會一直占着進程號,操作系統的進程數量有限則會受影響。
解決僵屍進程: 一般僵屍進程的產生都是因為父進程的原因,則可以通過 kill 父進程解決,這時候僵屍進程就變成了孤兒進程,被 init 進程接收
0 號進程負責進程間的切換,解決CPU占用,1號進程,直接或者間接的創建進程,負責進程的創建和孤兒進程的善后
2. 線程
引入線程之后,進程只作為除CPU之外的系統資源的分配單元(如:打印機,內存地址空間等都是分配給進程的)
線程的是實現方式:
- 用戶級線程(User-Level Thread),用戶級線程由應用程序通過線程庫是實現如python (import thread), 線程的管理工作由應用程序負責。
- 內核級線程(kernel-Level Thread),內核級線程的管理工作由操作系統內核完成,線程調度,切換等工作都由內核負責,因此內核級線程的切換必然需要在核心態下才能完成
進程和線程的關系:一條線程指的是進程中一個單一順序的控制流,一個進程中可以並發多個線程,每條線程並行執行不同的任務。CPU的最小調度單元是線程,所以單進程多線程是可以利用多核CPU的。
線程的基本狀態:派生,阻塞,就緒,運行,結束。
- 新建(New):線程在進程內派生出來,它即可由進程派生,也可由線程派生。
- 阻塞(blocked):線程運行過程中,可能由於各種原因進入阻塞狀態
- 通過sleep方法進入睡眠狀態
- 線程調用一個在I/O上被阻塞的操作,即該操作在輸入輸出操作完成之前不會返到它的調用者
- 試圖得到一個鎖,而該鎖被其他線程持有
- 等待某個出發條件
- 就緒(ready):一個新創建的線程並不自動開始運行,要執行線程,必須調用線程的start()方法。當線程對象調用start()方法即啟動了線程,start()方法創建線程運行的系統資源,並調度線程運行run()方法。當start()方法返回后,線程就處於就緒狀態。處於就緒狀態的線程並不一定立即運行run()方法,線程還必須同其他線程競爭CPU時間,只有獲得CPU時間才可以運行線程
- 運行(running):線程后的CPU時間后,進入運行狀態,真正開始執行run()方法
- 死亡(dead): 征程退出自然死亡,或者異常終止導致線程猝死
2.1 線程模型:
- 用戶級線程模型(一對多模型)
多個用戶態的線程對應着一個內核線程,程序線程的創建、終止、切換或者同步等線程工作必須自身來完成。python就是這種。雖然可以實現異步,但是不能有效利用多核(GIL)
- 內核級線程模型 (一對一)
這種模型直接調用操作系統的內核線程,所有線程的創建、終止、切換、同步等操作,都由內核來完成。C++就是這種
- 兩級線程模型(M:N)
這種線程模型會先創建多個內核級線程,然后用自身的用戶級線程去對應創建的多個內核級線程,自身的用戶級線程需要本身程序去調度,內核級的線程交給操作系統內核去調度。GO語言就是這種。
python中的多線程因為GIL的存在,並不能利用多核CPU優勢,但是在阻塞的系統調用中,如sock.connect(), sock.recv()等耗時的I/O操作,當前的線程會釋放GIL,讓出處理器。但是單個線程內,阻塞調用上還是阻塞的。除了GIL之外,所有的多線程還有通病,他們都是被OS調用的,調度策略是搶占式的,以保證同等有限級的線程都有機執行,帶來的問題就是:並不知道下一刻執行那個線程,也不知道正在執行什么代碼,會存在競態條件
3. 協程
協程通過在線程中實現調度,避免了陷入內核級別的上下文切換造成的性能損失,進而突破了線程在IO上的性能瓶頸。
python的協程源於yield指令
- yield item 用於產出一個值,反饋給next()的調用方法
- 讓出處理機,暫停執行生成器,讓調用方繼續工作,直到需要使用另一個值時再調用next()
協程式對線程的調度,yield類似惰性求職方式可以視為一種流程控制工具,實現協作式多任務,python3.5引入了async/await表達式,使得協程證實在語言層面得到支持和優化,大大簡化之前的yield寫法。線程正式在語言層面得到支持和優化。線程是內核進行搶占式調度的,這樣就確保每個線程都有執行的機會。而coroutine運行在同一個線程中,有語言層面運行時中的EventLoop(事件循環)來進行調度。在python中協程的調度是非搶占式的,也就是說一個協程必須主動讓出執行機會,其他協程才有機會運行。讓出執行的關鍵字 await, 如果一個協程阻塞了,持續不讓出CPU處理機,那么整個線程就卡住了,沒有任何並發。
PS: 作為服務端,event loop最核心的就是I/O多路復用技術,所有來自客戶端的請求都由I/O多路復用函數來處理;作為客戶端,event loop的核心在於Future對象延遲執行,並使用send函數激發協程,掛起,等待服務端處理完成返回后再調用Callback函數繼續執行。[python 協程與go協程的區別]
3.1 Golang 協程
Go 天生在語言層面支持,和python類似都是用關鍵字,而GO語言使用了go關鍵字,go協程之間的通信,采用了channel關鍵字。
go實現了兩種並發形式:
- 多線程共享內存:如Java 或者C++在多線程中共享數據的時候,通過鎖來訪問
- Go語言特有的,也是Go語言推薦的 CSP(communicating sequential processes)並發模型。
package main
import ("fmt")
func main() {
jobs := make(chan int)
done := make(chan bool) // end flag
go func() {
for {
j, ok := <- jobs
fmt.Println("---->:", j, ok)
if ok {
fmt.Println("received job")
} else {
fmt.Println("end received jobs")
done <- true
return
}
}
}()
go func() {
for j:= 1; j <= 3; j++ {
jobs <-j
fmt.Println("sent job", j)
}
close(jobs)
fmt.Println("close(jobs)")
}()
fmt.Println("sent all jobs")
<-done // 阻塞 讓main等待協程完成
}
Go的CSP並發模型是通過goroutine 和 channel來實現的。
- goroutine是go語言中並發的執行單位。
- channel是Go語言中各個並發結構體之間的通信機制。
- channel -< data 寫數據
- <- channel 讀數據
協程本質上來說是一種用戶態的線程,不需要系統來執行搶占式調度,而是在語言測個面實現線程的調度。
4. 並發
並發:Do not communicate by sharing memory; instead, share memory by communicate.
4.1 Actor模型
Actor模型和CSP模型的區別:
- CSP並不Focus發送消息的實體/Task, 而是關注發送消息時消息所使用的載體,即channel。
- 在Actor的設計中,Actor與信箱是耦合的,而在CSP中channel是作為first-class獨立存在的
- Actor中有明確的send/receive關系,而channel中並不區分這樣的關系,執行快可以任意選擇發送或者取消息
4.4 Go 協程調度器 GPM
- G 指的是Goroutine,其本質上也是一種輕量級的線程
- P proessor, 代表M所需要的上下文環境,也是處理用戶級代碼邏輯處理器。同一時間只有一個線程(M)可以擁有P, P中的數據都是鎖自由(lock free)的, 讀寫這些數據的效率會非常的高
- M Machine,一個M直接關聯一個內核線程,可以運行go代碼 即goroutine, M運行go代碼需要一個P, 另外就是運行原生代碼,如 syscall。運行原生代碼不需要P。
一個M會對應一個內核線程,一個M也會連接一個上下文P,一個上下文P相當於一個“處理器”,一個上下文連接一個或者多個Goroutine。P(Processor)的數量是在啟動時被設置為環境變量GOMAXPROCS的值,或者通過運行時調用函數runtime.GOMAXPROCS()
進行設置
erlang和golang都是采用CSP模型,python中協程是eventloop模型。但是erlang是基於進程的消息通信,go是基於goroutine和channel通信。
python和golang都引入了消息調度系統模型,來避免鎖的影響和進程線程的開銷問題。
計算機科學領域的任何問題都可以通過增加一個間接的中間層來解決 -- G-P-M模型正是此理論踐行者,此理論也用到了python的asyncio對地獄回調的處理上(使用Task+Future避免回調嵌套),是不是巧合?
其實異步≈可中斷的函數+事件循環+回調,go和python都把嵌套結構轉換成列表結構有點像算法中的遞歸轉迭代.
調度器在計算機中是分配工作時所需要的資源,Linux的調度是CPU找到可運行的線程,Go的調度是為M線程找到P(內存,執行票據)和可運行的G(協程)
Go協程是輕量級的,棧初始2KB(OS操作系統的線程一般都是固有的棧內存2M), 調度不涉及系統調用,用戶函數調用前會檢查棧空間是否足夠,不夠的話,會進行站擴容,棧大小限制可以達到1GB。
Go的網絡操作是封裝了epoll, 為NonBlocking模式,切換協程不阻塞線程。
Go語言相比起其他語言的優勢在於OS線程是由OS內核來調度的,goroutine
則是由Go運行時(runtime)自己的調度器調度的,這個調度器使用一個稱為m:n調度的技術(復用/調度m個goroutine到n個OS線程)。 其一大特點是goroutine的調度是在用戶態下完成的, 不涉及內核態與用戶態之間的頻繁切換,包括內存的分配與釋放,都是在用戶態維護着一塊大的內存池, 不直接調用系統的malloc函數(除非內存池需要改變),成本比調度OS線程低很多。 另一方面充分利用了多核的硬件資源,近似的把若干goroutine均分在物理線程上, 再加上本身goroutine的超輕量,以上種種保證了go調度方面的性能。點我了解更多