Go語言雖然使用一個Go關(guān)鍵字即可實現(xiàn)并發(fā)編程舟误,但Goroutine被調(diào)度到后端之后蒙袍,具體的實現(xiàn)比較復(fù)雜扼褪。先看看調(diào)度器有哪幾部分組成嵌溢。
1、G
G是Goroutine的縮寫刻两,相當(dāng)于操作系統(tǒng)中的進(jìn)程控制塊,在這里就是Goroutine的控制結(jié)構(gòu)滴某,是對Goroutine的抽象磅摹。其中包括執(zhí)行的函數(shù)指令及參數(shù)滋迈;G保存的任務(wù)對象;線程上下文切換户誓,現(xiàn)場保護(hù)和現(xiàn)場恢復(fù)需要的寄存器(SP饼灿、IP)等信息。
Go不同版本Goroutine默認(rèn)棧大小不同帝美。
// Go1.11版本默認(rèn)stack大小為2KB
_StackMin = 2048
// 創(chuàng)建一個g對象,然后放到g隊列
// 等待被執(zhí)行
func newproc1(fn *funcval, argp *uint8, narg int32, callergp *g, callerpc uintptr) {_g_ := getg() ? ?_g_.m.locks++ ? ?siz := narg ? ?siz = (siz +7) &^7_p_ := _g_.m.p.ptr() ? ?newg := gfget(_p_)
ifnewg ==nil{
// 初始化g stack大小newg = malg(_StackMin) ? ? ? ?casgstatus(newg, _Gidle, _Gdead) ? ? ? ?allgadd(newg) ? ?}
// 以下省略}
2碍彭、M
M是一個線程或稱為Machine,所有M是有線程棧的证舟。如果不對該線程棧提供內(nèi)存的話硕旗,系統(tǒng)會給該線程棧提供內(nèi)存(不同操作系統(tǒng)提供的線程棧大小不同)。當(dāng)指定了線程棧女责,則M.stack→G.stack漆枚,M的PC寄存器指向G提供的函數(shù),然后去執(zhí)行抵知。
type m struct { ? ?
/* ? ? ? ?1. ?所有調(diào)用棧的Goroutine,這是一個比較特殊的Goroutine墙基。 ? ? ? ?2. ?普通的Goroutine棧是在Heap分配的可增長的stack,而g0的stack是M對應(yīng)的線程棧。 ? ? ? ?3. ?所有調(diào)度相關(guān)代碼,會先切換到該Goroutine的棧再執(zhí)行刷喜。 ? ?*/g0*gcurg*g//M當(dāng)前綁定的結(jié)構(gòu)體G//SP残制、PC寄存器用于現(xiàn)場保護(hù)和現(xiàn)場恢復(fù)vdsoSPuintptrvdsoPCuintptr// 省略…}
3、P
P(Processor)是一個抽象的概念掖疮,并不是真正的物理CPU初茶。所以當(dāng)P有任務(wù)時需要創(chuàng)建或者喚醒一個系統(tǒng)線程來執(zhí)行它隊列里的任務(wù)。所以P/M需要進(jìn)行綁定浊闪,構(gòu)成一個執(zhí)行單元恼布。
P決定了同時可以并發(fā)任務(wù)的數(shù)量,可通過GOMAXPROCS限制同時執(zhí)行用戶級任務(wù)的操作系統(tǒng)線程搁宾≌酃可以通過runtime.GOMAXPROCS進(jìn)行指定。在Go1.5之后GOMAXPROCS被默認(rèn)設(shè)置可用的核數(shù)盖腿,而之前則默認(rèn)為1爽待。
// 自定義設(shè)置GOMAXPROCS數(shù)量
func GOMAXPROCS(n int) int { ? ?
? ?/*1.GOMAXPROCS設(shè)置可執(zhí)行的CPU的最大數(shù)量,同時返回之前的設(shè)置。2.如果n <1,則不更改當(dāng)前的值翩腐。 ? ?*/ ? ?ret :=int(gomaxprocs) ? ?stopTheWorld("GOMAXPROCS")
// startTheWorld啟動時,使用newprocs鸟款。newprocs =int32(n) ? ?startTheWorld()
returnret}
// 默認(rèn)P被綁定到所有CPU核上
// P == cpu.cores
func getproccount() int32 { ? ?
? ?const maxCPUs = 64 * 1024varbuf [maxCPUs /8]byte// 獲取CPU Corer := sched_getaffinity(0, unsafe.Sizeof(buf), &buf[0]) ? ?n :=int32(0)
for_, v :=rangebuf[:r] {
forv !=0{ ? ? ? ? ? ?n +=int32(v &1) ? ? ? ? ? ?v >>=1} ? ?}
ifn ==0{ ? ? ? n =1}
returnn}
// 一個進(jìn)程默認(rèn)被綁定在所有CPU核上,返回所有CPU core。
// 獲取進(jìn)程的CPU親和性掩碼系統(tǒng)調(diào)用
// rax 204 ? ? ? ? ? ? ? ? ? ? ? ? ?; 系統(tǒng)調(diào)用碼
// system_call sys_sched_getaffinity; 系統(tǒng)調(diào)用名稱
// rid ?pid ? ? ? ? ? ? ? ? ? ? ? ? ; 進(jìn)程號
// rsi unsigned int len ? ? ? ? ? ?
// rdx unsigned long *user_mask_ptr
sys_linux_amd64.s:TEXT runtime·sched_getaffinity(SB),NOSPLIT,$0MOVQ ? ?pid+0(FP), DI ? ?MOVQlen+8(FP), SI ? ?MOVQ ? ?buf+16(FP), DX ? ?MOVL ? ?$SYS_sched_getaffinity, AX ? ?SYSCALL ? ?MOVL ? ?AX, ret+24(FP) ? ?RET
Go調(diào)度器調(diào)度過程
首先創(chuàng)建一個G對象茂卦,G對象保存到P本地隊列或者是全局隊列欠雌。P此時去喚醒一個M。P繼續(xù)執(zhí)行它的執(zhí)行序疙筹。M尋找是否有空閑的P富俄,如果有則將該G對象移動到它本身。接下來M執(zhí)行一個調(diào)度循環(huán)(調(diào)用G對象->執(zhí)行->清理線程→繼續(xù)找新的Goroutine執(zhí)行)而咆。
M執(zhí)行過程中霍比,隨時會發(fā)生上下文切換。當(dāng)發(fā)生上線文切換時暴备,需要對執(zhí)行現(xiàn)場進(jìn)行保護(hù)悠瞬,以便下次被調(diào)度執(zhí)行時進(jìn)行現(xiàn)場恢復(fù)。Go調(diào)度器M的棧保存在G對象上涯捻,只需要將M所需要的寄存器(SP浅妆、PC等)保存到G對象上就可以實現(xiàn)現(xiàn)場保護(hù)。當(dāng)這些寄存器數(shù)據(jù)被保護(hù)起來障癌,就隨時可以做上下文切換了凌外,在中斷之前把現(xiàn)場保存起來。如果此時G任務(wù)還沒有執(zhí)行完涛浙,M可以將任務(wù)重新丟到P的任務(wù)隊列康辑,等待下一次被調(diào)度執(zhí)行。當(dāng)再次被調(diào)度執(zhí)行時轿亮,M通過訪問G的vdsoSP疮薇、vdsoPC寄存器進(jìn)行現(xiàn)場恢復(fù)(從上次中斷位置繼續(xù)執(zhí)行)。
1我注、P 隊列
通過上圖可以發(fā)現(xiàn)按咒,P有兩種隊列:本地隊列和全局隊列。
本地隊列:當(dāng)前P的隊列但骨,本地隊列是Lock-Free励七,沒有數(shù)據(jù)競爭問題,無需加鎖處理嗽冒,可以提升處理速度呀伙。
全局隊列:全局隊列為了保證多個P之間任務(wù)的平衡。所有M共享P全局隊列添坊,為保證數(shù)據(jù)競爭問題剿另,需要加鎖處理。相比本地隊列處理速度要低于全局隊列贬蛙。
2雨女、上線文切換
簡單理解為當(dāng)時的環(huán)境即可,環(huán)境可以包括當(dāng)時程序狀態(tài)以及變量狀態(tài)阳准。例如線程切換的時候在內(nèi)核會發(fā)生上下文切換氛堕,這里的上下文就包括了當(dāng)時寄存器的值,把寄存器的值保存起來野蝇,等下次該線程又得到cpu時間的時候再恢復(fù)寄存器的值讼稚,這樣線程才能正確運(yùn)行括儒。
對于代碼中某個值說,上下文是指這個值所在的局部(全局)作用域?qū)ο笕裣搿O鄬τ谶M(jìn)程而言帮寻,上下文就是進(jìn)程執(zhí)行時的環(huán)境,具體來說就是各個變量和數(shù)據(jù)赠摇,包括所有的寄存器變量固逗、進(jìn)程打開的文件、內(nèi)存(堆棧)信息等藕帜。
3烫罩、線程清理
Goroutine被調(diào)度執(zhí)行必須保證P/M進(jìn)行綁定,所以線程清理只需要將P釋放就可以實現(xiàn)線程的清理洽故。什么時候P會釋放贝攒,保證其它G可以被執(zhí)行。P被釋放主要有兩種情況收津。
主動釋放:最典型的例子是饿这,當(dāng)執(zhí)行G任務(wù)時有系統(tǒng)調(diào)用,當(dāng)發(fā)生系統(tǒng)調(diào)用時M會處于Block狀態(tài)撞秋。調(diào)度器會設(shè)置一個超時時間长捧,當(dāng)超時時會將P釋放。
被動釋放:如果發(fā)生系統(tǒng)調(diào)用吻贿,有一個專門監(jiān)控程序串结,進(jìn)行掃描當(dāng)前處于阻塞的P/M組合。當(dāng)超過系統(tǒng)程序設(shè)置的超時時間舅列,會自動將P資源搶走肌割。去執(zhí)行隊列的其它G任務(wù)。
終于要來說說Golang中最吸引人的goroutine了帐要,這也是Golang能夠橫空出世的主要原因把敞。不同于Python基于進(jìn)程的并發(fā)模型,以及C++榨惠、Java等基于線程的并發(fā)模型奋早。Golang采用輕量級的goroutine來實現(xiàn)并發(fā),可以大大減少CPU的切換≡龋現(xiàn)在已經(jīng)有太多的文章來介紹goroutine的用法耽装,在這里,我們從源碼的角度來看看其內(nèi)部實現(xiàn)期揪。
重申一下重點:goroutine中的三個實體
goroutine中最主要的是三個實體為GMP掉奄,其中:
G:代表一個goroutine對象,每次go調(diào)用的時候凤薛,都會創(chuàng)建一個G對象姓建,它包括棧诞仓、指令指針以及對于調(diào)用goroutines很重要的其它信息,比如阻塞它的任何channel速兔,其主要數(shù)據(jù)結(jié)構(gòu):
typegstruct{stackstack// 描述了真實的棧內(nèi)存狂芋,包括上下界m*m// 當(dāng)前的mschedgobuf// goroutine切換時,用于保存g的上下文paramunsafe.Pointer// 用于傳遞參數(shù)憨栽,睡眠時其他goroutine可以設(shè)置param,喚醒時該goroutine可以獲取atomicstatusuint32stackLockuint32goidint64// goroutine的IDwaitsinceint64// g被阻塞的大體時間lockedm*m// G被鎖定只在這個m上運(yùn)行}
其中最主要的當(dāng)然是sched了翼虫,保存了goroutine的上下文屑柔。goroutine切換的時候不同于線程有OS來負(fù)責(zé)這部分?jǐn)?shù)據(jù),而是由一個gobuf對象來保存珍剑,這樣能夠更加輕量級掸宛,再來看看gobuf的結(jié)構(gòu):
typegobufstruct{spuintptrpcuintptrgguintptrctxtunsafe.Pointerretsys.Uintreglruintptrbpuintptr// for GOEXPERIMENT=framepointer}
其實就是保存了當(dāng)前的棧指針,計數(shù)器招拙,當(dāng)然還有g(shù)自身唧瘾,這里記錄自身g的指針是為了能快速的訪問到goroutine中的信息。
M:代表一個線程别凤,每次創(chuàng)建一個M的時候饰序,都會有一個底層線程創(chuàng)建;所有的G任務(wù)规哪,最終還是在M上執(zhí)行求豫,其主要數(shù)據(jù)結(jié)構(gòu):
typemstruct{g0*g// 帶有調(diào)度棧的goroutinegsignal*g// 處理信號的goroutinetls[6]uintptr// thread-local storagemstartfnfunc()curg*g// 當(dāng)前運(yùn)行的goroutinecaughtsigguintptrppuintptr// 關(guān)聯(lián)p和執(zhí)行的go代碼nextppuintptridint32mallocingint32// 狀態(tài)spinningbool// m是否out of workblockedbool// m是否被阻塞inwbbool// m是否在執(zhí)行寫屏蔽printlockint8incgobool// m在執(zhí)行cgo嗎fastranduint32ncgocalluint64// cgo調(diào)用的總數(shù)ncgoint32// 當(dāng)前cgo調(diào)用的數(shù)目parknotealllink*m// 用于鏈接allmschedlinkmuintptrmcache*mcache// 當(dāng)前m的內(nèi)存緩存lockedg*g// 鎖定g在當(dāng)前m上執(zhí)行,而不會切換到其他mcreatestack[32]uintptr// thread創(chuàng)建的棧}
結(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í)行层扶。也就是說線程的棧也是用的g實現(xiàn)箫章,而不是使用的OS的。
P:代表一個處理器镜会,每一個運(yùn)行的M都必須綁定一個P檬寂,就像線程必須在么一個CPU核上執(zhí)行一樣,由P來調(diào)度G在M上的運(yùn)行戳表,P的個數(shù)就是GOMAXPROCS(最大256)桶至,啟動時固定的昼伴,一般不修改;M的個數(shù)和P的個數(shù)不一定一樣多(會有休眠的M或者不需要太多的M)(最大10000)镣屹;每一個P保存著本地G任務(wù)隊列圃郊,也有一個全局G任務(wù)隊列。P的數(shù)據(jù)結(jié)構(gòu):
typepstruct{lockmutexidint32statusuint32// 狀態(tài)女蜈,可以為pidle/prunning/...linkpuintptrschedtickuint32// 每調(diào)度一次加1syscalltickuint32// 每一次系統(tǒng)調(diào)用加1sysmonticksysmontickmmuintptr// 回鏈到關(guān)聯(lián)的mmcache*mcacheracectxuintptrgoidcacheuint64// goroutine的ID的緩存goidcacheenduint64// 可運(yùn)行的goroutine的隊列runqheaduint32runqtailuint32runq[256]guintptrrunnextguintptr// 下一個運(yùn)行的gsudogcache[]*sudogsudogbuf[128]*sudogpallocpersistentAlloc// per-P to avoid mutexpad[sys.CacheLineSize]byte
其中P的狀態(tài)有Pidle, Prunning, Psyscall, Pgcstop, Pdead持舆;在其內(nèi)部隊列runqhead里面有可運(yùn)行的goroutine,P優(yōu)先從內(nèi)部獲取執(zhí)行的g伪窖,這樣能夠提高效率逸寓。
除此之外,還有一個數(shù)據(jù)結(jié)構(gòu)需要在這里提及覆山,就是schedt竹伸,可以看做是一個全局的調(diào)度者:
typeschedtstruct{goidgenuint64lastpolluint64lockmutexmidlemuintptr// idle狀態(tài)的mnmidleint32// idle狀態(tài)的m個數(shù)nmidlelockedint32// lockde狀態(tài)的m個數(shù)mcountint32// 創(chuàng)建的m的總數(shù)maxmcountint32// m允許的最大個數(shù)ngsysuint32// 系統(tǒng)中g(shù)oroutine的數(shù)目,會自動更新pidlepuintptr// idle的pnpidleuint32nmspinninguint32// 全局的可運(yùn)行的g隊列runqheadguintptrrunqtailguintptrrunqsizeint32// dead的G的全局緩存gflockmutexgfreeStack*ggfreeNoStack*gngfreeint32// sudog的緩存中心sudoglockmutexsudogcache*sudog}
大多數(shù)需要的信息都已放在了結(jié)構(gòu)體M簇宽、G和P中勋篓,schedt結(jié)構(gòu)體只是一個殼∥焊睿可以看到譬嚣,其中有M的idle隊列,P的idle隊列见妒,以及一個全局的就緒的G隊列孤荣。schedt結(jié)構(gòu)體中的Lock是非常必須的,如果M或P等做一些非局部的操作须揣,它們一般需要先鎖住調(diào)度器盐股。
goroutine的運(yùn)行過程
所有的goroutine都是由函數(shù)newproc來創(chuàng)建的,但是由于該函數(shù)不能調(diào)用分段棧耻卡,最后真正調(diào)用的是newproc1疯汁。在newproc1中主要進(jìn)行如下動作:
funcnewproc1(fn*funcval,argp*uint8,nargint32,nretint32,callerpcuintptr)*g{newg=malg(_StackMin)casgstatus(newg,_Gidle,_Gdead)allgadd(newg)newg.sched.sp=spnewg.stktopsp=spnewg.sched.pc=funcPC(goexit)+sys.PCQuantumnewg.sched.g=guintptr(unsafe.Pointer(newg))gostartcallfn(&newg.sched,fn)newg.gopc=callerpcnewg.startpc=fn.fn......}
分配一個g的結(jié)構(gòu)體
初始化這個結(jié)構(gòu)體的一些域
將g掛在就緒隊列
綁定g到一個m上
這個綁定只要m沒有突破上限GOMAXPROCS,就拿一個m綁定一個g。如果m的waiting隊列中有就從隊列中拿,否則就要新建一個m,調(diào)用newm卵酪。
funcnewm(fnfunc(),_p_*p){mp:=allocm(_p_,fn)mp.nextp.set(_p_)mp.sigmask=initSigmaskexecLock.rlock()newosproc(mp,unsafe.Pointer(mp.g0.stack.hi))execLock.runlock()}
該函數(shù)其實就是創(chuàng)建一個m幌蚊,跟newproc有些相似,之前也說了m在底層就是一個線程的創(chuàng)建溃卡,也即是newosproc函數(shù)溢豆,在往下挖可以看到會根據(jù)不同的OS來執(zhí)行不同的bsdthread_create函數(shù),而底層就是調(diào)用的runtime.clone:
clone(cloneFlags,stk,unsafe.Pointer(mp),unsafe.Pointer(mp.g0),unsafe.Pointer(funcPC(mstart)))
m創(chuàng)建好之后瘸羡,線程的入口是mstart漩仙,最后調(diào)用的即是mstart1:
funcmstart1(){_g_:=getg()gosave(&_g_.m.g0.sched)_g_.m.g0.sched.pc=^uintptr(0)asminit()minit()if_g_.m==&m0{initsig(false)}iffn:=_g_.m.mstartfn;fn!=nil{fn()}schedule()}
里面最重要的就是schedule了,在schedule中的動作大體就是找到一個等待運(yùn)行的g,然后然后搬到m上队他,設(shè)置其狀態(tài)為Grunning,直接切換到g的上下文環(huán)境,恢復(fù)g的執(zhí)行卷仑。
funcschedule(){_g_:=getg()if_g_.m.lockedg!=nil{stoplockedm()execute(_g_.m.lockedg,false)// Never returns.}}
schedule的執(zhí)行可以大體總結(jié)為:
schedule函數(shù)獲取g => [必要時休眠] => [喚醒后繼續(xù)獲取] => execute函數(shù)執(zhí)行g(shù) => 執(zhí)行后返回到goexit => 重新執(zhí)行schedule函數(shù)
簡單來說g所經(jīng)歷的幾個主要的過程就是:Gwaiting->Grunnable->Grunning。經(jīng)歷了創(chuàng)建,到掛在就緒隊列,到從就緒隊列拿出并運(yùn)行整個過程麸折。
casgstatus(gp,_Gwaiting,_Grunnable)casgstatus(gp,_Grunnable,_Grunning)
引入了struct M這層抽象锡凝。m就是這里的worker,但不是線程。處理系統(tǒng)調(diào)用中的m不會占用mcpu數(shù)量,只有干事的m才會對應(yīng)到線程.當(dāng)mcpu數(shù)量少于GOMAXPROCS時可以一直開新的線程干活.而goroutine的執(zhí)行則是在m和g都滿足之后通過schedule切換上下文進(jìn)入的.
搶占式調(diào)度
當(dāng)有很多goroutine需要執(zhí)行的時候垢啼,是怎么調(diào)度的了窜锯,上面說的P還沒有出場呢,在runtime.main中會創(chuàng)建一個額外m運(yùn)行sysmon函數(shù)芭析,搶占就是在sysmon中實現(xiàn)的衬浑。
sysmon會進(jìn)入一個無限循環(huán), 第一輪回休眠20us, 之后每次休眠時間倍增, 最終每一輪都會休眠10ms. sysmon中有netpool(獲取fd事件), retake(搶占), forcegc(按時間強(qiáng)制執(zhí)行g(shù)c), scavenge heap(釋放自由列表中多余的項減少內(nèi)存占用)等處理.
funcsysmon(){lasttrace:=int64(0)idle:=0// how many cycles in succession we had not wokeup somebodydelay:=uint32(0)for{ifidle==0{// start with 20us sleep...delay=20}elseifidle>50{// start doubling the sleep after 1ms...delay*=2}ifdelay>10*1000{// up to 10msdelay=10*1000}usleep(delay)......}}
里面的函數(shù)retake負(fù)責(zé)搶占:
funcretake(nowint64)uint32{n:=0fori:=int32(0);i0&&pd.syscallwhen+10*1000*1000>now{continue}incidlelocked(-1)ifatomic.Cas(&_p_.status,s,_Pidle){iftrace.enabled{traceGoSysBlock(_p_)traceProcStop(_p_)}n++_p_.syscalltick++handoffp(_p_)}incidlelocked(1)}elseifs==_Prunning{// 如果G運(yùn)行時間過長,則搶占該Gt:=int64(_p_.schedtick)ifint64(pd.schedtick)!=t{pd.schedtick=uint32(t)pd.schedwhen=nowcontinue}ifpd.schedwhen+forcePreemptNS>now{continue}preemptone(_p_)}}returnuint32(n)}
枚舉所有的P 如果P在系統(tǒng)調(diào)用中(_Psyscall), 且經(jīng)過了一次sysmon循環(huán)(20us~10ms), 則搶占這個P放刨, 調(diào)用handoffp解除M和P之間的關(guān)聯(lián), 如果P在運(yùn)行中(_Prunning), 且經(jīng)過了一次sysmon循環(huán)并且G運(yùn)行時間超過forcePreemptNS(10ms), 則搶占這個P
并設(shè)置g.preempt = true尸饺,g.stackguard0 = stackPreempt进统。
為什么設(shè)置了stackguard就可以實現(xiàn)搶占?
因為這個值用于檢查當(dāng)前棧空間是否足夠, go函數(shù)的開頭會比對這個值判斷是否需要擴(kuò)張棧浪听。
newstack函數(shù)判斷g.stackguard0等于stackPreempt, 就知道這是搶占觸發(fā)的, 這時會再檢查一遍是否要搶占螟碎。
搶占機(jī)制保證了不會有一個G長時間的運(yùn)行導(dǎo)致其他G無法運(yùn)行的情況發(fā)生。
總結(jié)
相比大多數(shù)并行設(shè)計模型迹栓,Go比較優(yōu)勢的設(shè)計就是P上下文這個概念的出現(xiàn)掉分,如果只有G和M的對應(yīng)關(guān)系,那么當(dāng)G阻塞在IO上的時候克伊,M是沒有實際在工作的酥郭,這樣造成了資源的浪費(fèi),沒有了P愿吹,那么所有G的列表都放在全局不从,這樣導(dǎo)致臨界區(qū)太大,對多核調(diào)度造成極大影響犁跪。
而goroutine在使用上面的特點椿息,感覺既可以用來做密集的多核計算,又可以做高并發(fā)的IO應(yīng)用坷衍,做IO應(yīng)用的時候寝优,寫起來感覺和對程序員最友好的同步阻塞一樣,而實際上由于runtime的調(diào)度枫耳,底層是以同步非阻塞的方式在運(yùn)行(即IO多路復(fù)用)乏矾。
所以說保護(hù)現(xiàn)場的搶占式調(diào)度和G被阻塞后傳遞給其他m調(diào)用的核心思想,使得goroutine的產(chǎn)生。