虛擬存儲涉及到的相關基礎知識總結 1
821
2025-04-04
一生很短,Let's Go
人生苦短,我用Python
Golang、Golang、Golang 真的夠浪,今天我們一起盤點一下Golang并發那些事兒,準確來說是goroutine,關于多線程并發,咱們暫時先放一放(主要是俺現在還不太會,不敢出來瞎搞)。關于golang優點如何,咱們也不扯那些虛的。反正都是大佬在說,俺只是個吃瓜群眾,偶爾打打醬油,逃~。
說到并發,等等一系列的騷概念就出來了,為了做個照顧一下自己的菜,順便復習一下
基礎概念
進程
進程(英語:process),是指計算機中已運行的程序。進程曾經是`分時系統的基本運作單位。在面向進程設計的系統(如早期的UNIX,Linux 2.4及更早的版本)中,進程是程序的基本執行實體;在面向線程設計的系統(如當代多數操作系統、Linux?2.6及更新的版本)中,進程本身不是基本運行單位,而是線程的容器。
程序本身只是指令、數據及其組織形式的描述,相當于一個名詞,進程才是程序(那些指令和數據)的真正運行實例,可以想像說是現在進行式。若干進程有可能與同一個程序相關系,且每個進程皆可以同步或異步的方式獨立運行。現代計算機系統可在同一段時間內以進程的形式將多個程序加載到存儲器中,并借由時間共享(或稱時分復用),以在一個處理器上表現出同時平行性運行的感覺。同樣的,使用多線程技術(多線程即每一個線程都代表一個進程內的一個獨立執行上下文)的操作系統或計算機體系結構,同樣程序的平行線程,可在多CPU主機或網絡上真正同時運行(在不同的CPU上)。
操作系統需要有一種方式來創建進程。
以下4種主要事件會創建進程
系統初始化 (簡單可理解為關機后的開機)
正在運行的程序執行了創建進程的系統調用(例如:朋友發了一個網址,你點擊后開啟瀏覽器進入網頁中)
用戶請求創建一個新進程(例如:打開一個程序,打開QQ、微信)
一個批量作業的初始化
進程在創建后,開始運行與處理相關任務。但并不會永恒存在,終究會完成或退出。那么以下四種情況會發生進程的終止
正常退出(自愿)
錯誤退出(自愿)
崩潰退出(非自愿)
被其他殺死(非自愿)
正常退出:你退出瀏覽器,你點了一下它
錯誤退出:你此時正在津津有味的看著電視劇,突然程序內部發生bug,導致退出
崩潰退出:你程序崩潰了
被其他殺死:例如在windows上,使用任務管理器關閉進程
運行態(實際占用CPU)
就緒態(可運行、但其他進程正在運行而暫停)
阻塞態(除非某種外部的時間發生,否則進程不能運行)
前兩種狀態在邏輯上是類似的。處于這兩種狀態的進程都可以運行,只是對于第二種狀態暫時沒有分配CPU,一旦分配到了CPU即可運行
第三種狀態與前兩種不同,處于該狀態的進程不能運行,即是CPU空閑也不行。
如有興趣,可進一步了解進程的實現、多進程設計模型
進程池技術的應用至少由以下兩部分組成:
資源進程
預先創建好的空閑進程,管理進程會把工作分發到空閑進程來處理。
管理進程
管理進程負責創建資源進程,把工作交給空閑資源進程處理,回收已經處理完工作的資源進程。
資源進程跟管理進程的概念很好理解,管理進程如何有效的管理資源進程,分配任務給資源進程,回收空閑資源進程,管理進程要有效的管理資源進程,那么管理進程跟資源進程間必然需要交互,通過IPC,信號,信號量,消息隊列,管道等進行交互。
進程池:準確來說它并不實際存在于我們的操作系統中,而是IPC,信號,信號量,消息隊列,管道等對多進程進行管理,從而減少不斷的開啟、關閉等操作。以求達到減少不必要的資源損耗
線程
線程(英語:thread)是操作系統能夠進行運算調度的最小單位。大部分情況下,它被包含在進程之中,是進程中的實際運作單位。一條線程指的是進程中一個單一順序的控制流,一個進程中可以并發多個線程,每條線程并行執行不同的任務。在Unix System V及SunOS中也被稱為輕量進程(lightweight processes),但輕量進程更多指內核線程(kernel thread),而把用戶線程(user thread)稱為線程。
線程是獨立調度和分派的基本單位。線程可以為操作系統內核調度的內核線程
同一進程中的多條線程將共享該進程中的全部系統資源,如虛擬地址空間,文件描述符和信號處理等等。但同一進程中的多個線程有各自的調用棧(call stack),自己的寄存器環境(register context),自己的線程本地存儲(thread-local storage)。
一個進程可以有很多線程來處理,每條線程并行執行不同的任務。如果進程要完成的任務很多,這樣需很多線程,也要調用很多核心,在多核或多CPU,或支持Hyper-threading的CPU上使用多線程程序設計的好處是顯而易見的,即提高了程序的執行吞吐率。以人工作的樣子想像,核心相當于人,人越多則能同時處理的事情越多,而線程相當于手,手越多則工作效率越高。在單CPU單核的計算機上,使用多線程技術,也可以把進程中負責I/O處理、人機交互而常被阻塞的部分與密集計算的部分分開來執行,編寫專門的workhorse線程執行密集計算,雖然多任務比不上多核,但因為具備多線程的能力,從而提高了程序的執行效率。
線程池(英語:thread pool):一種線程使用模式。線程過多會帶來調度開銷,進而影響緩存局部性和整體性能。而線程池維護著多個線程,等待著監督管理者分配可并發執行的任務。這避免了在處理短時間任務時創建與銷毀線程的代價。線程池不僅能夠保證內核的充分利用,還能防止過分調度。可用線程數量應該取決于可用的并發處理器、處理器內核、內存、網絡sockets等的數量。 例如,線程數一般取cpu數量+2比較合適,線程數過多會導致額外的線程切換開銷。
任務調度以執行線程的常見方法是使用同步隊列,稱作任務隊列。池中的線程等待隊列中的任務,并把執行完的任務放入完成隊列中。
線程池模式一般分為兩種:HS/HA半同步/半異步模式、L/F領導者與跟隨者模式。
半同步/半異步模式又稱為生產者消費者模式,是比較常見的實現方式,比較簡單。分為同步層、隊列層、異步層三層。同步層的主線程處理工作任務并存入工作隊列,工作線程從工作隊列取出任務進行處理,如果工作隊列為空,則取不到任務的工作線程進入掛起狀態。由于線程間有數據通信,因此不適于大數據量交換的場合。
線程池的伸縮性對性能有較大的影響。
創建太多線程,將會浪費一定的資源,有些線程未被充分使用。
銷毀太多線程,將導致之后浪費時間再次創建它們。
創建線程太慢,將會導致長時間的等待,性能變差。
銷毀線程太慢,導致其它線程資源饑餓。
協程
協程,英文叫作 Coroutine,又稱微線程、纖程,協程是一種用戶態的輕量級線程。
協程擁有自己的寄存器上下文和棧。協程調度切換時,將寄存器上下文和棧保存到其他地方,在切回來的時候,恢復先前保存的寄存器上下文和棧。因此協程能保留上一次調用時的狀態,即所有局部狀態的一個特定組合,每次過程重入時,就相當于進入上一次調用的狀態。
協程本質上是個單進程,協程相對于多進程來說,無需線程上下文切換的開銷,無需原子操作鎖定及同步的開銷,編程模型也非常簡單。
串行
多個任務,執行完畢后再執行另一個。
例如:吃完飯后散步(先坐下吃飯、吃完后去散步)
并行
多個任務、交替執行
例如:做飯,一會放水洗菜、一會吸收(菜比較臟,洗下菜寫下手,傲嬌~)
并發
共同出發
邊吃飯、邊看電視
阻塞與非阻塞
阻塞狀態指程序未得到所需計算資源時被掛起的狀態。程序在等待某個操作完成期間,自身無法繼續處理其他的事情,則稱該程序在該操作上是阻塞的。
常見的阻塞形式有:網絡 I/O 阻塞、磁盤 I/O 阻塞、用戶輸入阻塞等。阻塞是無處不在的,包括 CPU 切換上下文時,所有的進程都無法真正處理事情,它們也會被阻塞。如果是多核 CPU 則正在執行上下文切換操作的核不可被利用。
程序在等待某操作過程中,自身不被阻塞,可以繼續處理其他的事情,則稱該程序在該操作上是非阻塞的。
非阻塞并不是在任何程序級別、任何情況下都可以存在的。僅當程序封裝的級別可以囊括獨立的子程序單元時,它才可能存在非阻塞狀態。
非阻塞的存在是因為阻塞存在,正因為某個操作阻塞導致的耗時與效率低下,我們才要把它變成非阻塞的。
同步與異步
不同程序單元為了完成某個任務,在執行過程中需靠某種通信方式以協調一致,我們稱這些程序單元是同步執行的。
例如購物系統中更新商品庫存,需要用“行鎖”作為通信信號,讓不同的更新請求強制排隊順序執行,那更新庫存的操作是同步的。
簡言之,同步意味著有序。
為完成某個任務,不同程序單元之間過程中無需通信協調,也能完成任務的方式,不相關的程序單元之間可以是異步的。
例如,爬蟲下載網頁。調度程序調用下載程序后,即可調度其他任務,而無需與該下載任務保持通信以協調行為。不同網頁的下載、保存等操作都是無關的,也無需相互通知協調。這些異步操作的完成時刻并不確定。
可異步與不可異步
經過以上了解,又是進程、又是線程、等等一系列的騷東西,那是真的難受。不過相信你已經有個初步的概率,那么這里我們將更加深入的去了解可異步與不可異步。
在此之前先總結一下,以上各種演進的路線,其實加速無非就是一句話,提高效率。(廢話~)
那么提高效率的是兩大因素,增加投入以求增加產出、盡可能避免不必要的損耗(例如:減少上下文切換等等)。
如何區分它是可異步代碼還是不可異步呢,其實很簡單那就是,它是否能夠自主完成不需要我們參與的部分。
我們從結果反向思考,
例如我們發送一個網絡請求,這之間擁有網絡I/O阻塞,那么測試我們將它掛起、轉而去做其他事情,等他響應了,我們在進行此階段的下一步的操作。那么這個是可異步的
另外:寫作業與上洗手間,我此時正在寫著作業,突然,我想上洗手間了,走。上完洗手間后又回來繼續寫作業,在我去洗手間這段時間作業是不會有任何進展,所以我們可以理解為這是非異步
goroutine
東扯一句,西扯一句,終于該上真家伙了,廢話不多說。
如何實現只需定義很多個任務,讓系統去幫助我們把這些任務分配到CPU上實現并發執行。
Go語言中的goroutine就是這樣一種機制,goroutine的概念類似于線程,但?goroutine是由Go的運行時(runtime)調度和管理的。Go程序會智能地將 goroutine 中的任務合理地分配給每個CPU。Go語言之所以被稱為現代化的編程語言,就是因為它在語言層面已經內置了調度和上下文切換的機制。
在Go語言編程中你不需要去自己寫進程、線程、協程,你的技能包里只有一個技能–goroutine,當你需要讓某個任務并發執行的時候,你只需要把這個任務包裝成一個函數,開啟一個goroutine去執行這個函數就可以了
goroutine與線程
可增長的棧
OS線程(操作系統線程)一般都有固定的棧內存(通常為2MB),一個goroutine的棧在其生命周期開始時只有很小的棧(典型情況下2KB),goroutine的棧不是固定的,他可以按需增大和縮小,goroutine的棧大小限制可以達到1GB,雖然極少會用到這么大。所以在Go語言中一次創建十萬左右的goroutine也是可以的。
goroutine模型
GPM是Go語言運行時(runtime)層面的實現,是go語言自己實現的一套調度系統。區別于操作系統調度OS線程。
G很好理解,就是個goroutine的,里面除了存放本goroutine信息外 還有與所在P的綁定等信息。
P管理著一組goroutine隊列,P里面會存儲當前goroutine運行的上下文環境(函數指針,堆棧地址及地址邊界),P會對自己管理的goroutine隊列做一些調度(比如把占用CPU時間較長的goroutine暫停、運行后續的goroutine等等)當自己的隊列消費完了就去全局隊列里取,如果全局隊列里也消費完了會去其他P的隊列里搶任務。
M(machine)是Go運行時(runtime)對操作系統內核線程的虛擬, M與內核線程一般是一一映射的關系, 一個groutine最終是要放到M上執行的;
P與M一般也是一一對應的。他們關系是: P管理著一組G掛載在M上運行。當一個G長久阻塞在一個M上時,runtime會新建一個M,阻塞G所在的P會把其他的G 掛載在新建的M上。當舊的G阻塞完成或者認為其已經死掉時 回收舊的M。
P的個數是通過runtime.GOMAXPROCS設定(最大256),Go1.5版本之后默認為物理線程數。 在并發量大的時候會增加一些P和M,但不會太多,切換太頻繁的話得不償失。
單從線程調度講,Go語言相比起其他語言的優勢在于OS線程是由OS內核來調度的,goroutine則是由Go運行時(runtime)自己的調度器調度的,這個調度器使用一個稱為m:n調度的技術(復用/調度m個goroutine到n個OS線程)。 其一大特點是goroutine的調度是在用戶態下完成的, 不涉及內核態與用戶態之間的頻繁切換,包括內存的分配與釋放,都是在用戶態維護著一塊大的內存池, 不直接調用系統的malloc函數(除非內存池需要改變),成本比調度OS線程低很多。 另一方面充分利用了多核的硬件資源,近似的把若干goroutine均分在物理線程上, 再加上本身goroutine的超輕量,以上種種保證了go調度方面的性能。
GOMAXPROCS
Go運行時的調度器使用GOMAXPROCS參數來確定需要使用多少個OS線程來同時執行Go代碼。默認值是機器上的CPU核心數。例如在一個8核心的機器上,調度器會把Go代碼同時調度到8個OS線程上(GOMAXPROCS是m:n調度中的n)。
Go語言中可以通過runtime.GOMAXPROCS()函數設置當前程序并發時占用的CPU邏輯核心數。
Go1.5版本之前,默認使用的是單核心執行。Go1.5版本之后,默認使用全部的CPU邏輯核心數。
goroutine的創建
使用goroutine非常簡單,只需要在調用函數的時在函數名前面加上go關鍵字,就可以為一個函數創建一個goroutine。
一個goroutine必定對應一個函數,當然也可以創建多個goroutine去執行相同的函數。
語法如下
func?main()?{
go?函數()[普通函數和匿名函數即可]
}
如果你此時興致勃勃的想立馬試試,我只想和你說,“少俠,請稍等~”,我話還沒說完。以上我只說了如何創建goroutine,可沒說這樣就是這樣用的。嘻嘻~
首先我們先看看不用goroutine的代碼,示例如下
#?example
package?main
import?(
"fmt"
"time"
)
func?example(i?int)?{
//fmt.Println("HelloWord~,?stamp?is",?i)
time.Sleep(time.Second)
}
//?normal
func?main()?{
startTime?:=?time.Now()
for?i?:=?0;?i?10;?i++?{
example(i)
}
fmt.Println("Main~")
spendTime?:=?time.Since(startTime)
fmt.Println("Spend?Time:",?spendTime)
}
輸入結果如下
那么我們來使用goroutine,運行
示例代碼如下:
package?main
import?(
"fmt"
"time"
)
func?example(i?int)?{
fmt.Println("HelloWord~,?stamp?is",?i)
time.Sleep(time.Second)
}
//?normal
func?main()?{
startTime?:=?time.Now()
//?創建十個goroutine
for?i?:=?0;?i?10;?i++?{
go?example(i)
}
fmt.Println("Main~")
spendTime?:=?time.Since(startTime)
fmt.Println("Spend?Time:",?spendTime)
}
輸出如下
乍一看,好家伙速度提升了簡直不是一個量級啊,秒啊~
仔細看你會發現,7,9 跑去哪兒呢?不見了,盯~
謎底在下一篇揭曉~
期待下一篇,盤點Golang并發那些事兒之二,goroutine并發控制得心應手
Go 任務調度 多線程
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。