深入Golang調(diào)度器之GMP模型

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)生。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末妻熊,一起剝皮案震驚了整個濱河市夸浅,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌扔役,老刑警劉巖帆喇,帶你破解...
    沈念sama閱讀 211,743評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異亿胸,居然都是意外死亡坯钦,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評論 3 385
  • 文/潘曉璐 我一進(jìn)店門侈玄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來婉刀,“玉大人,你說我怎么就攤上這事序仙⊥患眨” “怎么了?”我有些...
    開封第一講書人閱讀 157,285評論 0 348
  • 文/不壞的土叔 我叫張陵潘悼,是天一觀的道長律秃。 經(jīng)常有香客問我,道長治唤,這世上最難降的妖魔是什么棒动? 我笑而不...
    開封第一講書人閱讀 56,485評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮宾添,結(jié)果婚禮上船惨,老公的妹妹穿的比我還像新娘。我一直安慰自己缕陕,他們只是感情好粱锐,可當(dāng)我...
    茶點故事閱讀 65,581評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著扛邑,像睡著了一般卜范。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上鹿榜,一...
    開封第一講書人閱讀 49,821評論 1 290
  • 那天海雪,我揣著相機(jī)與錄音,去河邊找鬼舱殿。 笑死奥裸,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的沪袭。 我是一名探鬼主播湾宙,決...
    沈念sama閱讀 38,960評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼樟氢,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了侠鳄?” 一聲冷哼從身側(cè)響起埠啃,我...
    開封第一講書人閱讀 37,719評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎伟恶,沒想到半個月后碴开,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,186評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡博秫,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,516評論 2 327
  • 正文 我和宋清朗相戀三年潦牛,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片挡育。...
    茶點故事閱讀 38,650評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡巴碗,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出即寒,到底是詐尸還是另有隱情橡淆,我是刑警寧澤,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布母赵,位于F島的核電站明垢,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏市咽。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,936評論 3 313
  • 文/蒙蒙 一抵蚊、第九天 我趴在偏房一處隱蔽的房頂上張望施绎。 院中可真熱鬧,春花似錦贞绳、人聲如沸谷醉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽俱尼。三九已至,卻和暖如春萎攒,著一層夾襖步出監(jiān)牢的瞬間遇八,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評論 1 266
  • 我被黑心中介騙來泰國打工耍休, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留刃永,地道東北人。 一個月前我還...
    沈念sama閱讀 46,370評論 2 360
  • 正文 我出身青樓羊精,卻偏偏與公主長得像斯够,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,527評論 2 349