Goroutine是Go里的一種輕量級線程——協(xié)程。相對線程,協(xié)程的優(yōu)勢就在于它非常輕量級,進(jìn)行上下文切換的代價非常的小。對于一個goroutine ,每個結(jié)構(gòu)體G中有一個sched的屬性就是用來保存它上下文的。這樣,goroutine 就可以很輕易的來回切換。由于其上下文切換在用戶態(tài)下發(fā)生,根本不必進(jìn)入內(nèi)核態(tài),所以速度很快。而且只有當(dāng)前goroutine 的 PC, SP等少量信息需要保存。
在Go語言中,每一個并發(fā)的執(zhí)行單元為一個goroutine。當(dāng)我們開始運(yùn)行一個Go程序時,它的入口函數(shù) main 實際上就是運(yùn)行在一個goroutine 里。
Goroutine之間的通信
Go 語言編寫的程序通過不同的goroutine 運(yùn)行,但是goroutine之間是相互獨立的,各自運(yùn)行在不同的上下文中。 每個 goroutine 之間的通信需要借助 channel ,channel 是Go 語言里的一種通信機(jī)制。Channel 也是Go語言里的一種引用類型,通過make函數(shù),我們可以很容易的聲明一個channel。
ch:=make(chanstring)
Channel 有單方向,雙方向之分:
chan int, 雙方向,可用來收發(fā)數(shù)據(jù)。
chan <- int,單方向,只能用來發(fā)送數(shù)據(jù)
<- chan int,單方向,只能用來接收數(shù)據(jù)
另外,channel還有有緩存無緩存之分。通過make函數(shù)創(chuàng)建一個帶緩存的channel。
ch:=make(chanstring, n)
無緩存的channel保證了每次發(fā)送數(shù)據(jù)的同步接收操作。而帶緩存的channel解耦了發(fā)送與接收間的操作,這樣不但是影響程序的性能還有可能引起死鎖的問題。
調(diào)度器sheduler
每個goroutine的運(yùn)行都是由Go語言里的調(diào)度器(scheduler)決定的。
先說操作系統(tǒng)的線程調(diào)度。在POSIX 中有一個sheduler的內(nèi)核函數(shù),每過幾ms會被執(zhí)行一次。每次執(zhí)行時,會掛起當(dāng)前執(zhí)行線程,同時保存它寄存器中信息,接著查看線程列表決定下一個線程的運(yùn)行, 從內(nèi)存中必復(fù)其寄存器信息和現(xiàn)場并開始執(zhí)行。不同線程之間存在上下文切換,這包括保存一個用戶線程的狀態(tài)到內(nèi)存,恢復(fù)另一個線程的信息到寄存器,同時還要更新sheduler相關(guān)的數(shù)據(jù)結(jié)構(gòu)。這些操作都很耗時。
Go 語言的Runtime有自己的sheduler,通過它我們可以在n個操作系統(tǒng)的線程上調(diào)度m個goroutine。實際上Go 的sheduler與操作系統(tǒng)的sheduler是非常相似的,只不過它只關(guān)心goroutine的調(diào)度。與操作系統(tǒng)sheduler不同的是,Go的sheduler不使用硬件定時器,當(dāng)一個goroutine 調(diào)用了time.Sleep、觸發(fā)一個channel 操作或者使用 mutex, scheduler 會使這個 goroutine 進(jìn)行睡眠,進(jìn)而去喚醒另外一個goroutine,這種調(diào)度方式?jīng)]有上下文之間的切換,它的代價比操作系統(tǒng)的線程調(diào)度要小得多。
Go的調(diào)度的實現(xiàn),涉及到幾個重要的數(shù)據(jù)結(jié)構(gòu)。運(yùn)行時庫用這幾個數(shù)據(jù)結(jié)構(gòu)來實現(xiàn)goroutine的調(diào)度,管理goroutine和物理線程的運(yùn)行。這些數(shù)據(jù)結(jié)構(gòu)分別是結(jié)構(gòu)體G,結(jié)構(gòu)體M,結(jié)構(gòu)體P,以及 Sched 結(jié)構(gòu)體。這三個結(jié)構(gòu)定義在文件 runtime/runtime.h 中,而Sched的定義在 runtime/proc.c 中。在Go語言中scheduler 通過一個GOMAXPROCS變量來決定有多少個操作系統(tǒng)的線程來運(yùn)行Go程序,默認(rèn)值為CPU的核心數(shù)。
結(jié)構(gòu)體G
G 是 goroutine 的縮寫,相當(dāng)于操作系統(tǒng)中的進(jìn)程控制塊,在這里就是 goroutine 的控制結(jié)構(gòu),是對goroutine的抽象。其中包括 goid 是這個 goroutine 的ID, status 是這個goroutine 的狀態(tài),如 Gidle, Grunnable, Grunning, Gsyscall, Gwaiting,Gdead 等。
structG
{
uintptr stackguard; // 分段棧的可用空間下界
uintptr stackbase; // 分段棧的?;? Gobuf sched; //進(jìn)程切換時,利用sched域來保存上下文
uintptr stack0;
FuncVal* fnstart; // goroutine運(yùn)行的函數(shù)
void* param; // 用于傳遞參數(shù),睡眠時其它goroutine設(shè)置param,喚醒時此goroutine可以獲取
int16 status; // 狀態(tài) Gidle,Grunnable,Grunning,Gsyscall,Gwaiting,Gdead
int64 goid; // goroutine的id號
G* schedlink;
M* m; // for debuggers, but offset not hard-coded
M* lockedm; // G被鎖定只能在這個m上運(yùn)行
uintptr gopc; // 創(chuàng)建這個goroutine的go表達(dá)式的pc
...
};
結(jié)構(gòu)體G中的部分域如上所示??梢钥吹?,其中包含了棧信息stackbase和stackguard,有運(yùn)行的函數(shù)信息fnstart。這些就足夠成為一個可執(zhí)行的單元了,只要得到CPU就可以運(yùn)行。
goroutine 切換時,上下文信息保存在結(jié)構(gòu)體的 sched 域中。goroutine 是輕量級的線程或者稱為協(xié)程,切換時并不必陷入到操作系統(tǒng)內(nèi)核中,所以保存過程很輕量??匆幌陆Y(jié)構(gòu)體 G 中的 Gobuf,其實只保存了當(dāng)前棧指針,程序計數(shù)器,以及 goroutine 自身。
structGobuf
{
// The offsets of these fields are known to (hard-coded in) libmach.
uintptr sp;
byte* pc;
G* g;
...
};
記錄g是為了恢復(fù)當(dāng)前goroutine的結(jié)構(gòu)體G指針,運(yùn)行時庫中使用了一個常駐的寄存器extern register G* g,這個是當(dāng)前goroutine的結(jié)構(gòu)體G的指針。這樣做是為了快速地訪問goroutine中的信息,比如,Go的棧的實現(xiàn)并沒有使用%ebp寄存器,不過這可以通過g->stackbase快速得到。"extern register"是由6c,8c等實現(xiàn)的一個特殊的存儲。在ARM上它是實際的寄存器;其它平臺是由段寄存器進(jìn)行索引的線程本地存儲的一個槽位。在linux系統(tǒng)中,對g和m使用的分別是0(GS)和4(GS)。需要注意的是,鏈接器還會根據(jù)特定操作系統(tǒng)改變編譯器的輸出,例如,6l/linux下會將0(GS)重寫為-16(FS)。每個鏈接到Go程序的C文件都必須包含runtime.h頭文件,這樣C編譯器知道避免使用專用的寄存器。
結(jié)構(gòu)體M
M是machine的縮寫,是對機(jī)器的抽象,每個m都是對應(yīng)到一條操作系統(tǒng)的物理線程。M必須關(guān)聯(lián)了P才可以執(zhí)行Go代碼,但是當(dāng)它處理阻塞或者系統(tǒng)調(diào)用中時,可以不需要關(guān)聯(lián)P。
structM
{
G* g0; // 帶有調(diào)度棧的goroutine
G* gsignal; // signal-handling G 處理信號的goroutine
void (*mstartfn)(void);
G* curg; // M中當(dāng)前運(yùn)行的goroutine
P* p; // 關(guān)聯(lián)P以執(zhí)行Go代碼 (如果沒有執(zhí)行Go代碼則P為nil)
P* nextp;
int32 id;
int32 mallocing; //狀態(tài)
int32 throwing;
int32 gcing;
int32 locks;
int32 helpgc; //不為0表示此m在做幫忙gc。helpgc等于n只是一個編號
bool blockingsyscall;
bool spinning;
Note park;
M* alllink; // 這個域用于鏈接allm
M* schedlink;
MCache *mcache;
G* lockedg;
M* nextwaitm; // next M waiting for lock
GCStats gcstats;
...
};
和G類似,M中也有alllink域?qū)⑺械腗放在allm鏈表中。lockedg是某些情況下,G鎖定在這個M中運(yùn)行而不會切換到其它M中去。M中還有一個MCache,是當(dāng)前M的內(nèi)存的緩存。M也和G一樣有一個常駐寄存器變量,代表當(dāng)前的M。同時存在多個M,表示同時存在多個物理線程。結(jié)構(gòu)體M中有兩個G是需要關(guān)注一下的,一個是curg,代表結(jié)構(gòu)體M當(dāng)前綁定的結(jié)構(gòu)體G。另一個是g0,是帶有調(diào)度棧的goroutine,這是一個比較特殊的goroutine。普通的goroutine的棧是在堆上分配的可增長的棧,而g0的棧是M對應(yīng)的線程的棧。所有調(diào)度相關(guān)的代碼,會先切換到該goroutine的棧中再執(zhí)行。
結(jié)構(gòu)體P
Go1.1中新加入的一個數(shù)據(jù)結(jié)構(gòu),它是Processor的縮寫。結(jié)構(gòu)體P的加入是為了提高Go程序的并發(fā)度,實現(xiàn)更好的調(diào)度。M代表OS線程。P代表Go代碼執(zhí)行時需要的資源。當(dāng)M執(zhí)行Go代碼時,它需要關(guān)聯(lián)一個P,當(dāng)M為idle或者在系統(tǒng)調(diào)用中時,它也需要P。有剛好GOMAXPROCS個P。所有的P被組織為一個數(shù)組,在P上實現(xiàn)了工作流竊取的調(diào)度器。
structP
{
Lock;
uint32 status; // Pidle或Prunning等
P* link;
uint32 schedtick; // 每次調(diào)度時將它加一
M* m; // 鏈接到它關(guān)聯(lián)的M (nil if idle)
MCache* mcache;
G* runq[256];
int32 runqhead;
int32 runqtail;
// Available G's (status == Gdead)
G* gfree;
int32 gfreecnt;
byte pad[64];
};
結(jié)構(gòu)體P中也有相應(yīng)的狀態(tài):Pidle, Prunning, Psyscall, Pgcstop, Pdead。跟G不同的是,P 不存在waiting狀態(tài)。跟G不同的是,P不存在waiting狀態(tài)。MCache被移到了P中,但是在結(jié)構(gòu)體M中也還保留著。在P中有一個Grunnable的goroutine隊列,這是一個P的局部隊列。當(dāng)P執(zhí)行Go代碼時,它會優(yōu)先從自己的這個局部隊列中取,這時可以不用加鎖,提高了并發(fā)度。如果發(fā)現(xiàn)這個隊列空了,則去其它P的隊列中拿一半過來,這樣實現(xiàn)工作流竊取的調(diào)度。這種情況下是需要給調(diào)用器加鎖的。
結(jié)構(gòu)體Sched
Sched是調(diào)度實現(xiàn)中使用的數(shù)據(jù)結(jié)構(gòu),該結(jié)構(gòu)體的定義在文件proc.c中。
structSched {
Lock;
uint64 goidgen;
M* midle; // idle m's waiting for work
int32 nmidle; // number of idle m's waiting for work
int32 nmidlelocked; // number of locked m's waiting for work
int3 mcount; // number of m's that have been created
int32 maxmcount; // maximum number of m's allowed (or die)
P* pidle; // idle P's
uint32 npidle; //idle P的數(shù)量
uint32 nmspinning;
// Global runnable queue.
G* runqhead;
G* runqtail;
int32 runqsize;
// Global cache of dead G's.
Lock gflock;
G* gfree;
int32 stopwait;
Note stopnote;
uint32 sysmonwait;
Note sysmonnote;
uint64 lastpoll;
int32 profilehz; // cpu profiling rate
}
大多數(shù)需要的信息都已放在了結(jié)構(gòu)體M、G和P中,Sched結(jié)構(gòu)體只是一個殼??梢钥吹剑渲杏蠱的idle隊列,P的idle隊列,以及一個全局的就緒的G隊列。Sched結(jié)構(gòu)體中的Lock是非常必須的,如果M或P等做一些非局部的操作,它們一般需要先鎖住調(diào)度器。
Goroutine的特點
Goroutine是Go Runtime所提供的,并非操作系統(tǒng)層面上支持的,goroutine不是用線程實現(xiàn)的。goroutine就是一段代碼,一個函數(shù)入口,以及在堆上為其分配的一個堆棧。這個棧通常很小,一般為2kB, 所以它非常廉價,我們可以很輕松的創(chuàng)建上成千萬個goroutine,這是很普遍的。Goroutine 的棧大小不是固定的,這一點和操作系統(tǒng)的線程是不一樣的,它可以動態(tài)的擴(kuò)展,最大值可達(dá)1GB。
Goroutine是協(xié)作式調(diào)度的,如果goroutine會執(zhí)行很長時間,而且不是通過等待讀取或?qū)懭隿hannel的數(shù)據(jù)來同步的話,就需要主動調(diào)用Gosched()來讓出CPU。
Go語言封裝了異步IO,所以可以寫出貌似并發(fā)數(shù)很多的服務(wù)端,可即使我們通過調(diào)整GOMAXPROCS來充分利用多核CPU并行處理,其效率也不如我們利用IO事件驅(qū)動設(shè)計的、按照事務(wù)類型劃分好合適比例的線程池。在響應(yīng)時間上,協(xié)作式調(diào)度是硬傷。
每個goroutine是沒有身份標(biāo)識的,這是為了避免像Thread Local Storage那樣被爛用,一個函數(shù)的行為不可能由其本身變量決定。
Goroutine最大的優(yōu)點是在并發(fā)開發(fā)中實現(xiàn)了對線程池的動態(tài)擴(kuò)展,不會由于某個任務(wù)的阻塞而導(dǎo)致死鎖。隨著其運(yùn)行庫的不斷發(fā)展和完善及多核大行其道的年代,其優(yōu)勢會日益凸顯。
下面來看一個實例。
實例:生產(chǎn)者消費者問題
通過goroutine實現(xiàn)生產(chǎn)者消費者問題,利用 channel 通信。只需要短短幾行代碼,我們自己根本不需要編寫代碼考慮線程的同步問題。
需要事先聲明的變量,goods 是生產(chǎn)消費所共享的數(shù)據(jù),聲明為一個chan 類型,存放整型數(shù)據(jù)。接著聲明一個隨機(jī)數(shù)種子,根據(jù)系統(tǒng)時間生成偽隨機(jī)數(shù)。done 也是一個 chan 類型,里面只存儲一個空的struct,其作用是為了保證主線程在其它 goroutine 結(jié)束之后結(jié)束。
var goods chan int
var r = rand.New(rand.NewSource(time.Now().UnixNano()))//定義一個隨機(jī)數(shù)種子
vardone chan struct{}
生產(chǎn)者函數(shù),循環(huán)10次,依次向 goods里寫入1到10,每寫完一次后,隨機(jī)睡眠1~3秒。
funcproduce() {
for i:=1; i<=10; i++ {
goods <- i
time.Sleep(time.Duration(r.Int31n(3))*time.Second)
}
done <-struct{}{}
}
消費者函數(shù),循環(huán)5次從 goods 里取值,每讀完一次,隨機(jī)睡眠1~5秒。
funcconsume() {
for i:=0; i<5; i++ {
good := <- goods
fmt.Printf("The goods size is : %v\n",10-good+1)
time.Sleep(time.Duration(r.Int31n(5))*time.Second)
}
}
main 函數(shù)里開啟一個goroutine 運(yùn)行 produce 函數(shù),兩個goroutine 運(yùn)行consume 函數(shù)。
funcmain() {
goods =make(chanint)
done =make(chanstruct{})
goproduce()
goconsume()
goconsume()
<- done
}
output:
The goods size is :10
The goods size is :9
The goods size is :8
The goods size is :7
The goods size is :6
The goods size is :5
The goods size is :4
The goods size is :3
The goods size is :2
The goods size is :1
參考資料:
[深入解析Go](https://tiancaiamao.gitbooks.io/go-
internals/content/zh/05.1.html)