【譯文】原文地址
將goroutine從一個(gè)操作系統(tǒng)線程切換到另一個(gè)線程是有代價(jià)的晨雳,如果切換太頻繁會(huì)降低應(yīng)用程序的速度唠亚。隨著Go發(fā)展饭弓,調(diào)度器已經(jīng)解決了這個(gè)問題废士。在并發(fā)工作時(shí)淋肾,調(diào)度器提供goroutine和線程的親和性硫麻。先回顧幾年前的調(diào)度器來理解這種改進(jìn)過程。
Go老版本存在的問題
在Go 1.0&1.1早期樊卓,當(dāng)創(chuàng)建更多os線程(即將GOMAXPROCS值設(shè)置更大)來運(yùn)行并發(fā)程序時(shí)將會(huì)面臨性能下降的問題拿愧。讓我們從文檔中使用channel來計(jì)算質(zhì)數(shù)的例子開始:
package main
import "fmt"
//Send the sequence 2, 3, 4, ... to channel 'ch'.
func Generate(ch chan<- int) {
for i := 2; ;i++ {
ch <- i
}
}
//Copy the values from channel 'in' to channel 'out',
//removing those divisable by 'prime'.
func Filter(in <-chan int, out chan<- int, prime int) {
for{
i := <-in //Receive value from 'in'.
if i % prime !=0 {
out <- i //send 'i' to 'out'.
}
}
}
// The prime sieve: Daisy-chain Filter processes.
func main() {
ch := make(chan int)
go Generate(ch)
for i :=0; i <10; i++{
prime := <-ch
fmt.Println(prime)
ch1 := make(chan int)
go Filter(ch, ch1, prime)
ch = ch1
}
}
以下是Go 1.0.3版本,在不同GOMAXPROCS值下碌尔,計(jì)算10萬個(gè)質(zhì)數(shù)的基準(zhǔn)測(cè)試結(jié)果:
name time/op
Sieve 19.2s ± 0%
Sieve-2 19.3s ± 0%
Sieve-4 20.4s ± 0%
Sieve-8 20.4s ± 0%
要理解這些結(jié)果浇辜,我們需要理解此時(shí)調(diào)度器是如何設(shè)計(jì)的券敌。在Go第一個(gè)版本中,調(diào)度器只有一個(gè)全局隊(duì)列柳洋,所有的線程都可以推送和獲取goroutines待诅。下面是一個(gè)應(yīng)用程序?qū)嵗搼?yīng)用程序最多運(yùn)行兩個(gè)操作系統(tǒng)線程M,通過設(shè)置GOMAXPROCS=2來實(shí)現(xiàn):
只有一個(gè)隊(duì)列并不能保證goroutine將在同一個(gè)線程上恢復(fù)執(zhí)行熊镣。第一個(gè)線程準(zhǔn)備就緒卑雁,會(huì)獲取一個(gè)等待的goroutine運(yùn)行。因此轧钓,這里就會(huì)涉及到goroutine從一個(gè)線程到另一個(gè)線程的切換序厉,在性能方面會(huì)產(chǎn)生消耗。下面是一個(gè)阻塞式channel例子:
-
G7協(xié)程阻塞在channel上毕箍,等待channel中發(fā)送來的數(shù)據(jù)弛房。一旦channel有數(shù)據(jù)可接收,該協(xié)程會(huì)被推送到全局隊(duì)列當(dāng)中而柑。
-
然后文捶,channel推送消息,GX協(xié)程將在一個(gè)準(zhǔn)備就緒的線程上運(yùn)行媒咳,而G8協(xié)程將阻塞在channel上:
-
此時(shí) G7協(xié)程就會(huì)被調(diào)度到該線程上去:
Goroutine現(xiàn)在在不同的線程上運(yùn)行粹排。只有一個(gè)全局調(diào)度隊(duì)列會(huì)迫使調(diào)度程序只有一個(gè)互斥鎖來覆蓋所有的goroutine調(diào)度操作。以下是使用pprof工具獲取的CPU概況:
Total: 8679 samples
3700 42.6% 42.6% 3700 42.6% runtime.procyield
1055 12.2% 54.8% 1055 12.2% runtime.xchg
753 8.7% 63.5% 1590 18.3% runtime.chanrecv
677 7.8% 71.3% 677 7.8% dequeue
438 5.0% 76.3% 438 5.0% runtime.futex
367 4.2% 80.5% 5924 68.3% main.filter
234 2.7% 83.2% 5005 57.7% runtime.lock
230 2.7% 85.9% 3933 45.3% runtime.chansend
214 2.5% 88.4% 214 2.5% runtime.osyield
150 1.7% 90.1% 150 1.7% runtime.cas
procyield, xchg, futex和lock都與Go調(diào)度器的全局互斥量有關(guān)涩澡。很清楚的發(fā)現(xiàn)顽耳,應(yīng)用程序的很大部分時(shí)間花在鎖上。
這些問題導(dǎo)致Go在多處理器上沒有優(yōu)勢(shì)妙同,在Go1.1中已經(jīng)通過一個(gè)新的調(diào)度器解決了射富。
并發(fā)時(shí)的親和性
Go 1.1實(shí)現(xiàn)了一個(gè)新的調(diào)度器,并創(chuàng)建了本地調(diào)度隊(duì)列粥帚。如果有本地goroutines調(diào)度隊(duì)列并允許他們運(yùn)行在同一個(gè)OS線程上胰耗,這個(gè)改進(jìn)避免了鎖定整個(gè)調(diào)度程序。
由于線程可能在系統(tǒng)調(diào)用時(shí)阻塞芒涡,并且這種阻塞的線程數(shù)量是沒有限制的柴灯,Go引入了processes的概念。處理器P表示代表一個(gè)運(yùn)行的OS線程并管理本地goroutine調(diào)度隊(duì)列费尽。下面是新的模式:
如下是在Go 1.1.2版本使用新調(diào)度器運(yùn)行的基準(zhǔn)測(cè)試:
name time/op
Sieve 18.7s ± 0%
Sieve-2 8.26s ± 0%
Sieve-4 3.30s ± 0%
Sieve-8 2.64s ± 0%
Go現(xiàn)在可充分利用所有可用的CPU赠群。CPU使用概況也發(fā)生變化:
Total: 630 samples
163 25.9% 25.9% 163 25.9% runtime.xchg
113 17.9% 43.8% 610 96.8% main.filter
93 14.8% 58.6% 265 42.1% runtime.chanrecv
87 13.8% 72.4% 206 32.7% runtime.chansend
72 11.4% 83.8% 72 11.4% dequeue
19 3.0% 86.8% 19 3.0% runtime.memcopy64
17 2.7% 89.5% 225 35.7% runtime.chansend1
16 2.5% 92.1% 280 44.4% runtime.chanrecv2
12 1.9% 94.0% 141 22.4% runtime.lock
9 1.4% 95.4% 98 15.6% runqput
與鎖相關(guān)的大部分操作都已刪除,標(biāo)記為chanXXXX的操作只與channels相關(guān)依啰。但是乎串,如果調(diào)度程序改進(jìn)了goroutine和線程之間的親和性,那么在某些情況下,這種親和性需要降低叹誉。
限制親和性
要了解親和性的限制鸯两,我們必須了解何時(shí)會(huì)進(jìn)入本地隊(duì)列和全局隊(duì)列。本地隊(duì)列將用于除了系統(tǒng)調(diào)用外的所有操作长豁,例如阻塞在通道上和select操作钧唐,以及等待計(jì)時(shí)器和鎖,goroutine都會(huì)進(jìn)入本地調(diào)度隊(duì)列匠襟。然而钝侠,有兩個(gè)特例可以限制goroutine和線程的親和性:
- 工作竊取。當(dāng)處理器P在本地隊(duì)列沒有足夠的goroutine可調(diào)度酸舍,將會(huì)從其他P中竊取帅韧,并且全局隊(duì)列和網(wǎng)絡(luò)輪詢都為空。被竊取的goroutine就會(huì)在別的線程執(zhí)行啃勉。
- 系統(tǒng)調(diào)用忽舟。當(dāng)發(fā)生系統(tǒng)調(diào)用(如文件操作,http調(diào)用淮阐,數(shù)據(jù)庫(kù)操作等)叮阅,Go以阻塞模式掛起正在運(yùn)行的os線程,讓新的線程來處理當(dāng)前P上的本地隊(duì)列泣特。
但是浩姥,為了更好地管理本地隊(duì)列優(yōu)先級(jí),以上兩個(gè)約束可以避免状您。Go 1.5為了給goroutine在channel上來回通信提供更多的優(yōu)先級(jí)勒叠,因此通過指定線程以優(yōu)化親和性。
排序來提高親和性
goroutine在通道上來回通信導(dǎo)致頻繁的阻塞膏孟,例如頻繁在本地隊(duì)列中排隊(duì)缴饭。然而,由于本地隊(duì)列有一個(gè)FIFO實(shí)現(xiàn)骆莹,未阻塞的goroutine不能保證馬上得到運(yùn)行,如果線程被其他goroutine占用担猛。下面是一個(gè)關(guān)于一個(gè)之前被channel阻塞但現(xiàn)在可執(zhí)行的goroutine例子:
G9在被channel阻塞后恢復(fù)幕垦。但是,它必須等G2傅联、G5和G4才能執(zhí)行先改。在這個(gè)例子中,G5將占用線程導(dǎo)致G9延遲執(zhí)行蒸走,會(huì)導(dǎo)致G9被其他處理器竊取的風(fēng)險(xiǎn)仇奶。從Go 1.5開始,從通道中恢復(fù)的goroutine將優(yōu)先被執(zhí)行比驻,這主要?dú)w功于P的一個(gè)特殊屬性:
G9現(xiàn)在被標(biāo)記為下一個(gè)可執(zhí)行g(shù)oroutine该溯。這個(gè)新的優(yōu)先級(jí)允許goroutine從通道中恢復(fù)就馬上得到執(zhí)行岛抄。然后其他goroutine現(xiàn)在將有運(yùn)行時(shí)間。這一改變總體對(duì)Go標(biāo)準(zhǔn)庫(kù)產(chǎn)生積極影響狈茉,改善了一些包的性能夫椭。