GoLang并發(fā)機(jī)制探究goroutine原理詳細(xì)講解
通常程序會(huì)被編寫(xiě)為一個(gè)順序執(zhí)行并完成一個(gè)獨(dú)立任務(wù)的代碼。如果沒(méi)有特別的需求,最好總是這樣寫(xiě)代碼,因?yàn)檫@種類(lèi)型的程序通常很容易寫(xiě),也很容易維護(hù)。不過(guò)也有一些情況下,并行執(zhí)行多個(gè)任務(wù)會(huì)有更大的好處。一個(gè)例子是,Web 服務(wù)需要在各自獨(dú)立的套接字(socket)上同時(shí)接收多個(gè)數(shù)據(jù)請(qǐng)求。每個(gè)套接字請(qǐng)求都是獨(dú)立的,可以完全獨(dú)立于其他套接字進(jìn)行處理。具有并行執(zhí)行多個(gè)請(qǐng)求的能力可以顯著提高這類(lèi)系統(tǒng)的性能。考慮到這一點(diǎn),Go 語(yǔ)言的語(yǔ)法和運(yùn)行時(shí)直接內(nèi)置了對(duì)并發(fā)的支持。
1. 進(jìn)程與線(xiàn)程
當(dāng)運(yùn)行一個(gè)應(yīng)用程序的時(shí)候,操作系統(tǒng)會(huì)為這個(gè)應(yīng)用程序啟動(dòng)一個(gè)進(jìn)程。可以將這個(gè)進(jìn)程看作一個(gè)包含了應(yīng)用程序在運(yùn)行中需要用到和維護(hù)的各種資源的容器。這些資源包括但不限于內(nèi)存地址空間、文件和設(shè)備的句柄以及線(xiàn)程。
一個(gè)線(xiàn)程是一個(gè)執(zhí)行空間,這個(gè)空間會(huì)被 操作系統(tǒng)調(diào)度來(lái)運(yùn)行函數(shù)中所寫(xiě)的代碼。每個(gè)進(jìn)程至少包含一個(gè)線(xiàn)程,每個(gè)進(jìn)程的初始線(xiàn)程被稱(chēng)作主線(xiàn)程。因?yàn)閳?zhí)行這個(gè)線(xiàn)程的空間是應(yīng)用程序的本身的空間,所以當(dāng)主線(xiàn)程終止時(shí),應(yīng)用程序也會(huì)終止。操作系統(tǒng)將線(xiàn)程調(diào)度到某個(gè)處理器上運(yùn)行,這個(gè)處理器并不一定是進(jìn)程所在的處理器。不同操作系統(tǒng)使用的線(xiàn)程調(diào)度算法一般都不一樣,但是這種不同會(huì)被 操作系統(tǒng)屏蔽,并不會(huì)展示給程序員。
2. goroutine原理
Go 語(yǔ)言里的并發(fā)指的是能讓某個(gè)函數(shù)獨(dú)立于其他函數(shù)運(yùn)行的能力。操作系統(tǒng)會(huì)在物理處理器上調(diào)度線(xiàn)程來(lái)運(yùn)行,而Go語(yǔ)言中當(dāng)一個(gè)函數(shù)創(chuàng)建為goroutine時(shí),Go 會(huì)將其視為一個(gè)獨(dú)立的工作單元,這個(gè)單元會(huì)被調(diào)度到可用的邏輯處理器上執(zhí)行。每個(gè)邏輯處理器都分別綁定到單個(gè)操作系統(tǒng)線(xiàn)程。Go語(yǔ)言運(yùn)行時(shí)默認(rèn)會(huì)為每個(gè)可用的物理處理器分配一個(gè)邏輯處理器。
Go 語(yǔ)言運(yùn)行時(shí)的調(diào)度器是一個(gè)復(fù)雜的軟件,能管理被創(chuàng)建的所有g(shù)oroutine 并為其分配執(zhí)行時(shí)間。這個(gè)調(diào)度器在操作系統(tǒng)之上,將操作系統(tǒng)的線(xiàn)程與運(yùn)行時(shí)的邏輯處理器綁定,并在邏輯處理器上運(yùn)行g(shù)oroutine。調(diào)度器在任何給定的時(shí)間,都會(huì)全面控制哪個(gè)goroutine 要在哪個(gè)邏輯處理器上運(yùn)行。
下圖中可以看到操作系統(tǒng)線(xiàn)程、邏輯處理器和本地運(yùn)行隊(duì)列之間的關(guān)系。如果創(chuàng)建一個(gè)goroutine 并準(zhǔn)備運(yùn)行,這個(gè)goroutine 就會(huì)被放到調(diào)度器的全局運(yùn)行隊(duì)列中。之后,調(diào)度器就將這些隊(duì)列中的goroutine 分配給一個(gè)邏輯處理器,并放到這個(gè)邏輯處理器對(duì)應(yīng)的本地運(yùn)行隊(duì)列中。本地運(yùn)行隊(duì)列中的goroutine 會(huì)一直等待直到自己被分配的邏輯處理器執(zhí)行。
有時(shí),正在運(yùn)行的goroutine 需要執(zhí)行一個(gè)阻塞的系統(tǒng)調(diào)用,如打開(kāi)一個(gè)文件。當(dāng)這類(lèi)調(diào)用發(fā)生時(shí),線(xiàn)程和goroutine 會(huì)從邏輯處理器上分離,該線(xiàn)程會(huì)繼續(xù)阻塞,等待系統(tǒng)調(diào)用的返回。與此同時(shí),這個(gè)邏輯處理器就失去了用來(lái)運(yùn)行的線(xiàn)程。所以,調(diào)度器會(huì)創(chuàng)建一個(gè)新線(xiàn)程,并將其綁定到該邏輯處理器上。之后,邏輯處理器會(huì)從本地運(yùn)行隊(duì)列里選擇另一個(gè)goroutine 來(lái)運(yùn)行。一旦被阻塞的系統(tǒng)調(diào)用執(zhí)行完成并返回,對(duì)應(yīng)的goroutine 會(huì)放回到本地運(yùn)行隊(duì)列,而之前的線(xiàn)程會(huì)保存好,以便之后可以繼續(xù)使用。
如果一個(gè) goroutine 需要做一個(gè)網(wǎng)絡(luò)I/O 調(diào)用,流程上會(huì)有些不一樣。在這種情況下,goroutine會(huì)和邏輯處理器分離,并移到集成了網(wǎng)絡(luò)輪詢(xún)器的運(yùn)行時(shí)。一旦該輪詢(xún)器指示某個(gè)網(wǎng)絡(luò)讀或者寫(xiě)操作已經(jīng)就緒,對(duì)應(yīng)的goroutine 就會(huì)重新分配到邏輯處理器上來(lái)完成操作。調(diào)度器對(duì)可以創(chuàng)建的邏輯處理器的數(shù)量沒(méi)有限制,但語(yǔ)言運(yùn)行時(shí)默認(rèn)限制每個(gè)程序最多創(chuàng)建10 000 個(gè)線(xiàn)程。這個(gè)限制值可以通過(guò)調(diào)用runtime/debug 包的SetMaxThreads 方法來(lái)更改。如果程序試圖使用更多的線(xiàn)程,就會(huì)崩潰。
3. 并發(fā)與并行
并發(fā)(concurrency)不是并行(parallelism)。并行是讓不同的代碼片段同時(shí)在不同的物理處理器上執(zhí)行。并行的關(guān)鍵是同時(shí)做很多事情,而并發(fā)是指同時(shí)管理很多事情,這些事情可能只做了一半就被暫停去做別的事情了。在很多情況下,并發(fā)的效果比并行好,因?yàn)椴僮飨到y(tǒng)和硬件的總資源一般很少,但能支持系統(tǒng)同時(shí)做很多事情。這種“使用較少的資源做更多的事情”的哲學(xué),也是指導(dǎo)Go 語(yǔ)言設(shè)計(jì)的哲學(xué)。
如果希望讓goroutine 并行,必須使用多于一個(gè)邏輯處理器。當(dāng)有多個(gè)邏輯處理器時(shí),調(diào)度器會(huì)將goroutine 平等分配到每個(gè)邏輯處理器上。這會(huì)讓goroutine 在不同的線(xiàn)程上運(yùn)行。不過(guò)要想真的實(shí)現(xiàn)并行的效果,用戶(hù)需要讓自己的程序運(yùn)行在有多個(gè)物理處理器的機(jī)器上。否則,哪怕Go 語(yǔ)言運(yùn)行時(shí)使用多個(gè)線(xiàn)程,goroutine 依然會(huì)在同一個(gè)物理處理器上并發(fā)運(yùn)行,達(dá)不到并行的效果。
下圖展示了在一個(gè)邏輯處理器上并發(fā)運(yùn)行g(shù)oroutine 和在兩個(gè)邏輯處理器上并行運(yùn)行兩個(gè)并發(fā)的goroutine 之間的區(qū)別。調(diào)度器包含一些聰明的算法,這些算法會(huì)隨著Go 語(yǔ)言的發(fā)布被更新和改進(jìn),所以不推薦盲目修改語(yǔ)言運(yùn)行時(shí)對(duì)邏輯處理器的默認(rèn)設(shè)置。如果真的認(rèn)為修改邏輯處理器的數(shù)量可以改進(jìn)性能,也可以對(duì)語(yǔ)言運(yùn)行時(shí)的參數(shù)進(jìn)行細(xì)微調(diào)整。
3.1 在1個(gè)邏輯處理器上運(yùn)行Go程序
下面的代碼通過(guò)調(diào)用runtime 包的GOMAXPROCS 函數(shù),更改調(diào)度器只可以使用1個(gè)邏輯處理器。創(chuàng)建兩個(gè)goroutine,以并發(fā)的形式分別顯示大寫(xiě)和小寫(xiě)的英文字母:
package main import ( "fmt" "runtime" "sync" ) func main() { runtime.GOMAXPROCS(1) // 分配一個(gè)邏輯處理器給調(diào)度器使用 var wg sync.WaitGroup wg.Add(2) fmt.Println("Start Goroutines") go func() { defer wg.Done() for count := 0; count < 3; count++ { for char := 'a'; char < 'a'+26; char++ { fmt.Printf("%c ", char) } } }() go func() { defer wg.Done() for count := 0; count < 3; count++ { for char := 'A'; char < 'A'+26; char++ { fmt.Printf("%c ", char) } } }() fmt.Println("Waiting To Finish") wg.Wait() fmt.Println("\nTerminating Program") }
程序的輸出為:
Start Goroutines
Waiting To Finish
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z A B C
D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z a b c d e f g h i j k l m n
o p q r s t u v w x y z a b c d e f g h i j k l m n o p q r s t u v w x y z
Terminating Program
使用1個(gè)邏輯處理器,在同一個(gè)時(shí)刻實(shí)際只有一個(gè)線(xiàn)程在運(yùn)行,而且每個(gè)goroutine花費(fèi)的時(shí)間太短,并沒(méi)有發(fā)生goroutine的停止與重新調(diào)度,所以通過(guò)程序輸出可以看出每個(gè)goroutine在一個(gè)邏輯處理器上并發(fā)運(yùn)行的效果,他們看起來(lái)是順序執(zhí)行的。
3.2 goroutine的停止與重新調(diào)度
基于調(diào)度器的內(nèi)部算法,一個(gè)正運(yùn)行的goroutine 在工作結(jié)束前,可以被停止并重新調(diào)度。調(diào)度器這樣做的目的是防止某個(gè)goroutine 長(zhǎng)時(shí)間占用邏輯處理器。當(dāng)goroutine 占用時(shí)間過(guò)長(zhǎng)時(shí),調(diào)度器會(huì)停止當(dāng)前正運(yùn)行的goroutine,并給其他可運(yùn)行的goroutine 運(yùn)行的機(jī)會(huì)。
下圖從邏輯處理器的角度展示了這一場(chǎng)景。在第1 步,調(diào)度器開(kāi)始運(yùn)行g(shù)oroutine A,而goroutine B 在運(yùn)行隊(duì)列里等待調(diào)度。之后,在第2 步,調(diào)度器交換了goroutine A 和goroutine B。由于goroutine A 并沒(méi)有完成工作,因此被放回到運(yùn)行隊(duì)列。之后,在第3 步,goroutine B 完成了它的工作并被系統(tǒng)銷(xiāo)毀。這也讓goroutine A 繼續(xù)之前的工作。
下面的代碼中,同樣設(shè)置只使用1個(gè)邏輯處理器,程序創(chuàng)建了兩個(gè)goroutine,分別打印1~5000 內(nèi)的素?cái)?shù)。查找并顯示素?cái)?shù)會(huì)消耗不少時(shí)間,這會(huì)讓調(diào)度器有機(jī)會(huì)在第一個(gè)goroutine 找到所有素?cái)?shù)之前,切換該goroutine的時(shí)間片:
package main import ( "fmt" "runtime" "sync" ) var wg sync.WaitGroup func main() { runtime.GOMAXPROCS(1) // 分配一個(gè)邏輯處理器給調(diào)度器使用 wg.Add(2) // 創(chuàng)建兩個(gè)goroutine fmt.Println("Create Goroutines") go printPrime("A") go printPrime("B") fmt.Println("Waiting To Finish") wg.Wait() fmt.Println("Terminating Program") } // 顯示 5000 以?xún)?nèi)的素?cái)?shù)值 func printPrime(prefix string) { defer wg.Done() next: for outer := 2; outer < 5000; outer++ { for inner := 2; inner < outer; inner++ { if outer%inner == 0 { continue next } } fmt.Printf("%s:%d\n", prefix, outer) } fmt.Println("Completed", prefix) }
程序的輸出為:
Create Goroutines
Waiting To Finish
B:2
B:3
...
B:4583
B:4591
A:3 ** 切換 goroutine
A:5
...
A:4561
A:4567
B:4603 ** 切換 goroutine
B:4621
...
Completed B
A:4457 ** 切換 goroutine
A:4463
...
A:4993
A:4999
Completed A
Terminating Program
goroutine B 先顯示素?cái)?shù)。goroutine B 打印到素?cái)?shù)4591后,調(diào)度器就將正運(yùn)行的goroutine切換為goroutine A。之后goroutine A 在線(xiàn)程上執(zhí)行了一段時(shí)間,再次切換為goroutine B。這次goroutine B 完成了所有的工作。一旦goroutine B 返回,就會(huì)看到線(xiàn)程再次切換到goroutine A 并完成所有的工作。每次運(yùn)行這個(gè)程序,調(diào)度器切換的時(shí)間點(diǎn)都會(huì)稍微有些不同。
3.3 在多個(gè)邏輯處理器上運(yùn)行Go程序
如果給調(diào)度器分配多個(gè)邏輯處理器,我們會(huì)看到之前的示例程序的輸出行為會(huì)有些不同。下面的代碼中把邏輯處理器的數(shù)量改為2,讓我們看看打印英文字母的效果:
package main import ( "fmt" "runtime" "sync" ) func main() { runtime.GOMAXPROCS(2) // 分配2個(gè)邏輯處理器給調(diào)度器使用 var wg sync.WaitGroup wg.Add(2) fmt.Println("Start Goroutines") go func() { defer wg.Done() // 顯示小寫(xiě)字母表3 次 for count := 0; count < 3; count++ { for char := 'a'; char < 'a'+26; char++ { fmt.Printf("%c ", char) } } }() go func() { defer wg.Done() // 顯示大寫(xiě)字母表3 次 for count := 0; count < 3; count++ { for char := 'A'; char < 'A'+26; char++ { fmt.Printf("%c ", char) } } }() fmt.Println("Waiting To Finish") wg.Wait() fmt.Println("\nTerminating Program") }
程序輸出為:
Start Goroutines
Waiting To Finish
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z A B C D E F G H I J K L M N O P Q R S a b c d e f g h i j k l m
n o p q r s t u v w x y z a b c d e f g h i j k l m n o p q r s t u v w x y z a b c d e f g h i j k l m n o p q r s t u v w x y z T U
V W X Y Z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
Terminating Program
兩個(gè)goroutine 幾乎是同時(shí)開(kāi)始運(yùn)行的,大小寫(xiě)字母是混合在一起顯示的。所以每個(gè)goroutine 獨(dú)自運(yùn)行在自己的線(xiàn)程上。
到此這篇關(guān)于GoLang并發(fā)機(jī)制探究goroutine原理詳細(xì)講解的文章就介紹到這了,更多相關(guān)GoLang goroutine內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Golang 語(yǔ)言控制并發(fā) Goroutine的方法
- Go并發(fā)編程之goroutine使用正確方法
- Go并發(fā)的方法之goroutine模型與調(diào)度策略
- Go語(yǔ)言中的并發(fā)goroutine底層原理
- Go語(yǔ)言使用goroutine及通道實(shí)現(xiàn)并發(fā)詳解
- Golang并發(fā)繞不開(kāi)的重要組件之Goroutine詳解
- Go中Goroutines輕量級(jí)并發(fā)的特性及效率探究
- 詳解Go語(yǔ)言中如何通過(guò)Goroutine實(shí)現(xiàn)高并發(fā)
- golang并發(fā)編程中Goroutine 協(xié)程的實(shí)現(xiàn)
- Go 并發(fā)編程Goroutine的實(shí)現(xiàn)示例
相關(guān)文章
Go語(yǔ)言題解LeetCode599兩個(gè)列表的最小索引總和
這篇文章主要為大家介紹了Go語(yǔ)言題解LeetCode599兩個(gè)列表的最小索引總和示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12Go語(yǔ)言七篇入門(mén)教程二程序結(jié)構(gòu)與數(shù)據(jù)類(lèi)型
這篇文章主要為大家介紹了Go語(yǔ)言的程序結(jié)構(gòu)與數(shù)據(jù)類(lèi)型,本篇文章是Go語(yǔ)言七篇入門(mén)系列文,有需要的朋友可以借鑒參考下,希望能夠有所幫助2021-11-11golang定時(shí)器Timer的用法和實(shí)現(xiàn)原理解析
這篇文章主要介紹了golang定時(shí)器Ticker,本文主要來(lái)看一下Timer的用法和實(shí)現(xiàn)原理,需要的朋友可以參考以下內(nèi)容2023-04-04golang新手們?nèi)菀追傅?個(gè)錯(cuò)誤總結(jié)
這篇文章主要給大家介紹了關(guān)于golang新手們?nèi)菀追傅?個(gè)錯(cuò)誤,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-08-08