gmp
Go dilinin en büyük özelliklerinden biri yerel eşzamanlılık desteğidir. Sadece tek bir anahtar kelime ile bir coroutine başlatabilirsiniz, aşağıdaki örnekte gösterildiği gibi.
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
fmt.Println("hello world!")
}()
go func() {
defer wg.Done()
fmt.Println("hello world too!")
}()
wg.Wait()
}Go dilinin coroutine'leri kullanımı çok basittir, geliştiricilerden neredeyse hiç ekstra iş gerektirmez, bu da popülerliğinin nedenlerinden biridir. Ancak bu sadeliğin arkasında, her şeyi desteken önemsiz olmayan bir eşzamanlı zamanlayıcı vardır. Adı, çoğunuzun bir dereceye kadar duyduğuna inanıyorum. Çünkü ana katılımcıları G (coroutine), M (sistem iş parçacığı) ve P (işlemci) olduğu için, GMP zamanlayıcı olarak da bilinir. GMP zamanlayıcısının tasarımı tüm Go runtime tasarımını etkiler, GC ve ağ poller dahil. Tüm dilin en çekirdek kısmı olduğu söylenebilir. Bunun hakkında biraz bilgi sahibi olmak gelecekteki çalışmalarda yardımcı olabilir.
Tarihçe
Go dilinin eşzamanlı zamanlama modeli tamamen özgün değildir. Öncüllerinden dersler ve deneyimler emdi, mevcut haline evrildi ve geliştirdi. İlham aldığı diller şunları içerir:
- Occam - 1983
- Erlang - 1986
- Newsqueak - 1988
- Concurrent ML - 1993
- Alef - 1995
- Limbo - 1996
En önemli etki Hoare'un 1978 tarihli CSP (İletişimli Sıralı Süreçler) makalesinden geldi. Makalenin temel fikri, süreçlerin veri alışverişini iletişim yoluyla yaptığıdır. Yukarıda belirtilen tüm programlama dilleri CSP fikirlerinden etkilenmiştir. Erlang en tipik mesaj odaklı programlama dilidir ve ünlü açık kaynak mesaj kuyruğu ara katman yazılımı RabbitMQ Erlang ile yazılmıştır. Günümüzde bilgisayarların ve internetin gelişimiyle, eşzamanlılık desteği neredeyse modern dillerin standart özelliği haline geldi. Go dili, CSP fikirlerini birleştirerek buna göre doğdu.
Zamanlama Modeli
İlk olarak, GMP'nin üç bileşenini kısaca tanıtalım:
- G, Goroutine: Go dilindeki coroutine'leri ifade eder
- M, Machine: Sistem iş parçacıklarını veya işçi iş parçacıklarını ifade eder, işletim sistemi tarafından zamanlanır
- P, Processor: CPU işlemcilerini değil, Go tarafından soyut bir kavramı ifade eder, her sistem iş parçacığında coroutine'leri zamanlamak için çalışan işlemcileri ifade eder.
Coroutine'ler daha hafif iş parçacığı formudur, daha küçük ölçeklidir ve daha az kaynak gerektirir. Oluşturma, yok etme ve zamanlama zamanlaması tamamen Go runtime tarafından işlenir, işletim sistemi tarafından değil, bu yüzden yönetim maliyeti iş parçacıklarından çok daha düşüktür. Ancak coroutine'ler iş parçacıklarına bağlıdır. Coroutine'lerin yürütme zaman dilimleri iş parçacıklarından gelir ve iş parçacıklarının zaman dilimleri işletim sisteminden gelir. Farklı iş parçacıkları arasında geçiş yapmanın belirli bir maliyeti vardır. İş parçacıklarının zaman dilimlerini coroutine'ler için iyi kullanmanın yolu tasarımın anahtarıdır.
1:N
Bir problemi çözmenin en iyi yolu onu görmezden gelmektir. İş parçacığı geçişinin bir maliyeti varsa, geçiş yapmayın. Tüm coroutine'leri tek bir çekirdek iş parçacığına tahsis edin, böylece sadece coroutine geçişi olur.

İş parçacıkları ve coroutine'ler arasındaki ilişki 1:N'dir. Bu yaklaşımın çok belirgin bir dezavantajı vardır: günümüzün bilgisayarları neredeyse tamamen çok çekirdekli CPU'lar ve böyle bir tahsis çok çekirdekli CPU performansını tam olarak kullanamaz.
N:N
Başka bir yaklaşım: bir iş parçacığı bir coroutine'e karşılık gelir ve bir coroutine o iş parçacığının tüm zaman dilimlerinin tadını çıkarabilir. Birden fazla iş parçacığı çok çekirdekli CPU performansını da kullanabilir. Ancak iş parçacığı oluşturma ve geçiş maliyetleri nispeten yüksektir. Eğer bire bir ilişki ise, coroutine'lerin hafif doğasını iyi değerlendiremez.

M:N
M iş parçacığı N coroutine'e karşılık gelir, burada M N'den küçüktür. Birden fazla iş parçacığı birden fazla coroutine'e karşılık gelir, her iş parçacığı birkaç coroutine'e karşılık gelir. İşlemci P, coroutine'lerin G'nin iş parçacıklarının zaman dilimlerini nasıl kullanacağını zamanlamaktan sorumludur. Bu yöntem nispeten daha iyidir ve Go'nun bugüne kadar kullandığı zamanlama modelidir.
M sadece işlemci P ile ilişkilendirdikten sonra görevleri yürütebilir. Go GOMAXPROCS işlemci oluşturur, bu yüzden görevleri yürütmek için mevcut gerçek iş parçacığı sayısı GOMAXPROCS'tur. Varsayılan değeri mevcut makinedeki CPU mantıksal çekirdek sayısıdır, ancak manuel olarak değerini de ayarlayabiliriz.
- Kod aracılığıyla
runtime.GOMAXPROCS(N)ile değiştirin, runtime'da dinamik olarak ayarlanabilir. Çağırmak anında STW'ye neden olur. - Ortam değişkenini ayarlayın
export GOMAXPROCS=N, statik.
Gerçek durumlarda, M sayısı P sayısından büyüktür, çünkü runtime'da bazı sistem çağrıları gibi diğer görevleri işlemeleri gerekir. Maksimum değer 10000'dir.

GMP, bu üç katılımcı ve zamanlayıcının kendisi runtime'da karşılık gelen tip gösterimleri vardır. Hepsi runtime/runtime2.go dosyasında bulunur. Aşağıda yapılarını anlamayı kolaylaştırmak için kısaca tanıtılmaktadır.
G
G'nin runtime'daki gösterimi runtime.g yapısıdır, zamanlama modelindeki en temel zamanlama birimidir. Yapısı şöyledir (anlaşılması kolay olması için birçok alan atlanmıştır).
type g struct {
stack stack // offset known to runtime/cgo
_panic *_panic // innermost panic - offset known to liblink
_defer *_defer // innermost defer
m *m // current m; offset known to arm liblink
sched gobuf
goid uint64
waitsince int64 // approx time when the g become blocked
waitreason waitReason // if status==Gwaiting
atomicstatus atomic.Uint32
preempt bool // preemption signal, duplicates stackguard0 = stackpreempt
startpc uintptr // pc of goroutine function
parentGoid uint64 // goid of goroutine that created this goroutine
waiting *sudog // sudog structures this g is waiting on (that have a valid elem ptr); in lock order
}İlk alan bu coroutine'e ait yığının başlangıç ve bitiş bellek adresleridir:
type stack struct {
lo uintptr
hi uintptr
}_panic ve _defer sırasıyla panic yığını ve defer yığınına işaret eden işaretçilerdir:
_panic *_panic // innermost panic - offset known to liblink
_defer *_defer // innermost deferm şu anda bu g üzerinde yürüten coroutine'dir:
m *m // current m; offset known to arm liblinkpreempt mevcut coroutine'in önlenmesi gerekip gerekmediğini gösterir, g.stackguard0 = stackpreempt ile eşdeğerdir:
preempt bool // preemption signal, duplicates stackguard0 = stackpreemptatomicstatus coroutine G'nin durum değerini saklamak için kullanılır, aşağıdaki opsiyonel değerlere sahiptir:
| İsim | Açıklama |
|---|---|
_Gidle | Yeni tahsis edildi ve henüz başlatılmadı |
_Grunnable | Mevcut coroutine'in çalışabileceğini gösterir, bekleme kuyruğunda bulunur |
_Grunning | Mevcut coroutine'in kullanıcı kodunu yürüttüğünü gösterir |
_Gsyscall | Sistem çağrılarını yürütmek için bir M atandı |
_Gwaiting | Coroutine engellendi, engelleme nedeni aşağıya bakın |
_Gdead | Mevcut coroutine'in kullanılmadığını gösterir, yeni çıkmış veya yeni başlatılmış olabilir |
_Gcopystack | Coroutine yığını taşınıyor, bu süre zarfında kullanıcı kodu yürütülmez ve bekleme kuyruğunda değildir |
_Gpreempted | Kendisi engellendi önlemeye girer, önleyici tarafından uyandırılmayı bekler |
_Gscan | GC coroutine yığın alanını tarıyor, diğer durumlarla birlikte var olabilir |
sched coroutine bağlam bilgisini saklamak için kullanılır, coroutine'in yürütme durumunu geri yükler. sp, pc, ret işaretçilerini sakladığını görebilirsiniz.
type gobuf struct {
sp uintptr
pc uintptr
g guintptr
ctxt unsafe.Pointer
ret uintptr
lr uintptr
bp uintptr // for framepointer-enabled architectures
}waiting mevcut coroutine'in hangi coroutine'i beklediğini gösterir, waitsince coroutine'in ne zaman engellendiğinin zamanını kaydeder ve waitreason coroutine engelleme nedenini gösterir, opsiyonel değerler aşağıdaki gibidir.
var waitReasonStrings = [...]string{
waitReasonZero: "",
waitReasonGCAssistMarking: "GC assist marking",
waitReasonIOWait: "IO wait",
waitReasonChanReceiveNilChan: "chan receive (nil chan)",
waitReasonChanSendNilChan: "chan send (nil chan)",
waitReasonDumpingHeap: "dumping heap",
waitReasonGarbageCollection: "garbage collection",
waitReasonGarbageCollectionScan: "garbage collection scan",
waitReasonPanicWait: "panicwait",
waitReasonSelect: "select",
waitReasonSelectNoCases: "select (no cases)",
waitReasonGCAssistWait: "GC assist wait",
waitReasonGCSweepWait: "GC sweep wait",
waitReasonGCScavengeWait: "GC scavenge wait",
waitReasonChanReceive: "chan receive",
waitReasonChanSend: "chan send",
waitReasonFinalizerWait: "finalizer wait",
waitReasonForceGCIdle: "force gc (idle)",
waitReasonSemacquire: "semacquire",
waitReasonSleep: "sleep",
waitReasonSyncCondWait: "sync.Cond.Wait",
waitReasonSyncMutexLock: "sync.Mutex.Lock",
waitReasonSyncRWMutexRLock: "sync.RWMutex.RLock",
waitReasonSyncRWMutexLock: "sync.RWMutex.Lock",
waitReasonTraceReaderBlocked: "trace reader (blocked)",
waitReasonWaitForGCCycle: "wait for GC cycle",
waitReasonGCWorkerIdle: "GC worker (idle)",
waitReasonGCWorkerActive: "GC worker (active)",
waitReasonPreempted: "preempted",
waitReasonDebugCall: "debug call",
waitReasonGCMarkTermination: "GC mark termination",
waitReasonStoppingTheWorld: "stopping the world",
}goid ve parentGoid sırasıyla mevcut coroutine ve parent coroutine'in benzersiz tanımlayıcılarını temsil eder ve startpc mevcut coroutine'in giriş fonksiyonunun adresini temsil eder.
M
M runtime'da runtime.m yapısı olarak temsil edilir, bu işçi iş parçacıklarının soyutlamasıdır:
type m struct {
id int64
g0 *g // goroutine with scheduling stack
curg *g // current running goroutine
gsignal *g // signal-handling g
goSigStack gsignalStack // Go-allocated signal handling stack
p puintptr // attached p for executing go code (nil if not executing go code)
nextp puintptr
oldp puintptr // the p that was attached before executing a syscall
mallocing int32
throwing throwType
preemptoff string // if != "", keep curg running on this m
locks int32
dying int32
spinning bool // m is out of work and is actively looking for work
tls [tlsSlots]uintptr
...
}Benzer şekilde, M'nin birçok iç alanı vardır. Burada anlaşılması kolay olması için sadece bazı alanlar tanıtılmaktadır.
id: M'nin benzersiz tanımlayıcısıg0: Zamanlama yığınına sahip coroutinecurg: İşçi iş parçacığında çalışan kullanıcı coroutine'igsignal: İş parçacığı sinyallerini işlemekten sorumlu coroutinegoSigStack: Sinyal işleme için Go tarafından tahsis edilen yığın alanıp: İşlemci P'nin adresi,oldpsistem çağrısı yürütmeden önce P'ye işaret eder,nextpyeni tahsis edilen P'ye işaret edermallocing: Şu anda yeni bellek alanı tahsis edilip edilmediğini gösterirthrowing: M oluştuğunda hata tipini gösterirpreemptoff: Önleme tanımlayıcısı, boş dize olduğunda şu anda çalışan coroutine'in önlenebileceğini gösterirlocks: Mevcut M'nin "kilit" sayısını gösterir, 0 olmadığında önleme yasaktırdying: M'nin kurtarılamazpanicile karşılaştığını gösterir,[0,3]dört opsiyonel değeri vardır, düşükten yükseğe ciddiyeti gösterir.spinning: M'nin boş durumda olduğunu ve随时 müsait olduğunu gösterir.tls: İş parçacığı yerel depolama
P
P runtime'da runtime.p olarak temsil edilir, M ve G arasında zamanlama işinden sorumludur. Yapısı şöyledir:
type p struct {
id int32
status uint32 // one of pidle/prunning/...
schedtick uint32 // incremented on every scheduler call
syscalltick uint32 // incremented on every system call
sysmontick sysmontick // last tick observed by sysmon
m muintptr // back-link to associated m (nil if idle)
// Queue of runnable goroutines. Accessed without lock.
runqhead uint32
runqtail uint32
runq [256]guintptr
runnext guintptr
// Available G's (status == Gdead)
gFree struct {
gList
n int32
}
// preempt is set to indicate that this P should be enter the
// scheduler ASAP (regardless of what G is running on it).
preempt bool
...
}status P'nin durumunu gösterir, aşağıdaki opsiyonel değerlere sahiptir:
| Değer | Açıklama |
|---|---|
_Pidle | P boş durumda, zamanlayıcı tarafından M atanabilir veya diğer durumlar arasında geçiş yapıyor olabilir |
_Prunning | P M ile ilişkilendirilmiş ve kullanıcı kodunu yürütüyor |
_Psyscall | P ile ilişkilendirilmiş M'nin sistem çağrısı yaptığını gösterir, bu süre zarfında P diğer M tarafından önlenebilir |
_Pgcstop | P'nin GC nedeniyle durduğunu gösterir |
_Pdead | P'nin çoğu kaynağı alınır, artık kullanılmayacak |
Aşağıdaki alanlar P'nin runq yerel kuyruğunu kaydeder. Yerel kuyruğun maksimum boyutunun 256 olduğunu görebilirsiniz. Bu sayının üzerinde, G global kuyruğa yerleştirilir.
runqhead uint32
runqtail uint32
runq [256]guintptrrunnext bir sonraki mevcut G'yi gösterir:
runnext guintptrDiğer alanlar şu şekilde açıklanmıştır:
id: P'nin benzersiz tanımlayıcısıschedtick: Her coroutine zamanlaması ile artar,runtime.executefonksiyonunda görünür.syscalltick: Her sistem çağrısı ile artarsysmontick: Sistem izleyici tarafından son gözlemlenen bilgiyi kaydederm: P ile ilişkilendirilmiş MgFree: Boş G listesipreempt: P'nin tekrar zamanlamaya girmesi gerektiğini gösterir
Global kuyruk bilgisi runtime.schedt yapısında saklanır, bu zamanlayıcının runtime'daki gösterimidir, şöyledir.
type schedt struct {
...
midle muintptr // idle m's waiting for work
ngsys atomic.Int32 // number of system goroutines
pidle puintptr // idle p's
// Global runnable queue.
runq gQueue
runqsize int32
...
}Başlatma
Zamanlayıcı başlatma Go programlarının önyükleme aşamasında bulunur. Go programını önyüklemekten sorumlu fonksiyon runtime.rt0_go'dır, assembly ile uygulanır ve runtime/asm_*.s dosyasında bulunur. Kodun bir kısmı şöyledir:
TEXT runtime·rt0_go(SB),NOSPLIT|NOFRAME|TOPFRAME,$0
...
...
CALL runtime·check(SB)
MOVL 24(SP), AX // copy argc
MOVL AX, 0(SP)
MOVQ 32(SP), AX // copy argv
MOVQ AX, 8(SP)
CALL runtime·args(SB)
CALL runtime·osinit(SB)
CALL runtime·schedinit(SB)
// create a new goroutine to start program
MOVQ $runtime·mainPC(SB), AX // entry
PUSHQ AX
CALL runtime·newproc(SB)
POPQ AX
// start this M
CALL runtime·mstart(SB)
CALL runtime·abort(SB) // mstart should never return
RETAşağıdaki iki satırdan runtime·osinit ve runtime·schedinit çağrılarını görebilirsiniz.
CALL runtime·osinit(SB)
CALL runtime·schedinit(SB)İlki işletim sistemi ilgili işleri başlatmaktan sorumludur ve ikincisi zamanlayıcı başlatmadan sorumludur, bu runtime·schedinit fonksiyonudur. Program başladığında zamanlayıcı çalışması için gereken kaynakları başlatmaktan sorumludur. Aşağıda basitleştirilmiş kod bulunmaktadır.
func schedinit() {
...
gp := getg()
sched.maxmcount = 10000
// The world starts stopped.
worldStopped()
...
stackinit()
mallocinit()
mcommoninit(gp.m, -1)
lock(&sched.lock)
procs := ncpu
if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
procs = n
}
if procresize(procs) != nil {
throw("unknown runnable goroutine during bootstrap")
}
unlock(&sched.lock)
...
// World is effectively started now, as P's can run.
worldStarted()
...
}runtime.getg fonksiyonu assembly ile uygulanır. Fonksiyonu mevcut coroutine'in runtime gösterimini almaktır, bu runtime.g yapısına bir işaretçidir. sched.maxmcount = 10000 aracılığıyla, zamanlayıcı başlatıldığında M'nin maksimum sayısının 10000 olarak ayarlandığını görebilirsiniz. Bu değer sabittir ve değiştirilemez. Ondan sonra, yığın yığınını başlatır, sonra M'yi başlatmak için runtime.mcommoninit fonksiyonunu kullanır. Fonksiyon uygulaması şöyledir:
func mcommoninit(mp *m, id int64) {
gp := getg()
// g0 stack won't make sense for user (and is not necessary unwindable).
if gp != gp.m.g0 {
callers(1, mp.createstack[:])
}
lock(&sched.lock)
if id >= 0 {
mp.id = id
} else {
mp.id = mReserveID()
}
...
mpreinit(mp)
if mp.gsignal != nil {
mp.gsignal.stackguard1 = mp.gsignal.stack.lo + stackGuard
}
// Add to allm so garbage collector doesn't free g->m
// when it is just in a register or thread-local storage.
mp.alllink = allm
// NumCgoCall() iterates over allm w/o schedlock,
// so we need to publish it safely.
atomicstorep(unsafe.Pointer(&allm), unsafe.Pointer(mp))
unlock(&sched.lock)
...
}Bu fonksiyon M'yi ön başlatır, principalmente aşağıdaki işleri yapar:
- M'nin id'sini tahsis et
- İş parçacığı sinyallerini işlemek için ayrı bir G tahsis et,
runtime.mpreinitfonksiyonu tarafından tamamlandı - Global M bağlı listesinin
runtime.allmbaş düğümü olarak ekle
Sonra, P'yi başlat. Miktarı varsayılan olarak CPU mantıksal çekirdek sayısıdır, ardından ortam değişkeni değeri.
procs := ncpu
if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
procs = n
}
if procresize(procs) != nil {
throw("unknown runnable goroutine during bootstrap")
}Son olarak, runtime.procresize fonksiyonu P'yi başlatmaktan sorumludur. Geçen miktara göre tüm P'leri saklayan global dilimi runtime.allp değiştirir. İlk olarak, miktar boyutuna göre kapasiteyi genişletip genişletmeyeceğini belirler.
if nprocs > int32(len(allp)) {
// Synchronize with retake, which could be running
// concurrently since it doesn't run on a P.
lock(&allpLock)
if nprocs <= int32(cap(allp)) {
allp = allp[:nprocs]
} else {
nallp := make([]*p, nprocs)
// Copy everything up to allp's cap so we
// never lose old allocated Ps.
copy(nallp, allp[:cap(allp)])
allp = nallp
}
unlock(&allpLock)
}Sonra her P'yi başlat:
// initialize new P's
for i := old; i < nprocs; i++ {
pp := allp[i]
if pp == nil {
pp = new(p)
}
pp.init(i)
atomicstorep(unsafe.Pointer(&allp[i]), unsafe.Pointer(pp))
}Eğer mevcut coroutine tarafından kullanılan P'nin yok edilmesi gerekiyorsa, allp[0] ile değiştirilir ve runtime.acquirep fonksiyonu M ile yeni P arasındaki ilişkiyi tamamlar.
gp := getg()
if gp.m.p != 0 && gp.m.p.ptr().id < nprocs {
gp.m.p.ptr().status = _Prunning
gp.m.p.ptr().mcache.prepareForSweep()
} else {
if gp.m.p != 0 {
gp.m.p.ptr().m = 0
}
gp.m.p = 0
pp := allp[0]
pp.m = 0
pp.status = _Pidle
acquirep(pp)
}Sonra artık gerekli olmayan P'leri yok et. Yok etme sırasında, P'nin tüm kaynakları bırakılır, yerel kuyruğundaki tüm G global kuyruğa yerleştirilir ve yok ettikten sonra, allp dilimlenir.
// release resources from unused P's
for i := nprocs; i < old; i++ {
pp := allp[i]
pp.destroy()
// can't free P itself because it can be referenced by an M in syscall
}
// Trim allp.
if int32(len(allp)) != nprocs {
lock(&allpLock)
allp = allp[:nprocs]
unlock(&allpLock)
}Son olarak, boş P'leri bağlı listeye bağlayın ve sonunda listenin baş düğümünü döndürün:
var runnablePs *p
for i := nprocs - 1; i >= 0; i-- {
pp := allp[i]
if gp.m.p.ptr() == pp {
continue
}
pp.status = _Pidle
if runqempty(pp) {
pidleput(pp, now)
} else {
pp.m.set(mget())
pp.link.set(runnablePs)
runnablePs = pp
}
}
return runnablePsOndan sonra, zamanlayıcı başlatılır ve runtime.worldStarted tüm P'leri çalışmaya geri döndürür.
MOVQ $runtime·mainPC(SB), AX // entry
PUSHQ AX
CALL runtime·newproc(SB)
POPQ AX
// start this M
CALL runtime·mstart(SB)Sonra Go programını başlatmak için runtime.newproc fonksiyonu aracılığıyla yeni bir coroutine oluşturulur, ardından zamanlayıcının çalışmasını resmi olarak başlatmak için runtime.mstart çağırılır. Assembly ile de uygulanır ve dahili olarak runtime.mstart0 fonksiyonunu oluşturmak için çağırır. Fonksiyon kodunun bir kısmı şöyledir:
gp := getg()
osStack := gp.stack.lo == 0
if osStack {
size := gp.stack.hi
if size == 0 {
size = 16384 * sys.StackGuardMultiplier
}
gp.stack.hi = uintptr(noescape(unsafe.Pointer(&size)))
gp.stack.lo = gp.stack.hi - size + 1024
}
gp.stackguard0 = gp.stack.lo + stackGuard
gp.stackguard1 = gp.stackguard0
mstart1()Bu noktada, M'nin sadece bir coroutine'i g0 vardır, bu iş parçacığının sistem yığınını kullanır, ayrı tahsis edilen yığın alanı değil. mstart0 fonksiyonu ilk olarak G'nin yığın sınırını başlatır, sonra kalan başlatma işini tamamlamak için mstart1'e devreder.
gp := getg()
gp.sched.g = guintptr(unsafe.Pointer(gp))
gp.sched.pc = getcallerpc()
gp.sched.sp = getcallersp()
asminit()
minit()
if gp.m == &m0 {
mstartm0()
}
if fn := gp.m.mstartfn; fn != nil {
fn()
}
if gp.m != &m0 {
acquirep(gp.m.nextp.ptr())
gp.m.nextp = 0
}
schedule()Başlamadan önce, ilk olarak mevcut yürütme bağlamını kaydeder, çünkü başarılı başlatmadan sonra, zamanlama döngüsüne girer ve asla dönmez. Diğer çağrılar mstart1 fonksiyonundan dönmek için yürütme bağlamını yeniden kullanabilir, iş parçacığından çıkma amacına ulaşır. Kaydettikten sonra, runtime.asminit ve runtime.minit iki fonksiyonu sistem yığınını başlatmaktan sorumludur, sonra runtime.mstartm0 fonksiyonu sinyalleri işlemek için geri çağrıları ayarlar. Geri çağrı fonksiyonu m.mstartfn yürüttükten sonra, runtime.acquirep fonksiyonu M'yi daha önce oluşturulan P ile ilişkilendirir ve son olarak zamanlama döngüsüne girer.
Burada çağrılan runtime.schedule tüm Go runtime'ın ilk zamanlama döngüsüdür, zamanlayıcının resmi olarak çalışmaya başladığını temsil eder.
İş Parçacıkları
Zamanlayıcıda, G kullanıcı kodunu yürütmek için P'ye güvenmelidir ve P düzgün çalışmak için M ile ilişkilendirilmelidir. M sistem iş parçacıklarını ifade eder.
Oluşturma
M oluşturma runtime.newm fonksiyonu tarafından tamamlanır, bir fonksiyon, P ve id'yi parametre olarak kabul eder. Parametre olarak fonksiyon bir closure olamaz.
func newm(fn func(), pp *p, id int64) {
acquirem()
mp := allocm(pp, fn, id)
mp.nextp.set(pp)
mp.sigmask = initSigmask
newm1(mp)
releasem(getg().m)
}Başlamadan önce, newm ilk olarak iş parçacığının runtime gösterimini oluşturmak için runtime.allocm fonksiyonunu çağırır, bu M'dir. Süreçte, M'nin yığın sınırını başlatmak için runtime.mcommoninit fonksiyonunu kullanır.
func allocm(pp *p, fn func(), id int64) *m {
allocmLock.rlock()
// The caller owns pp, but we may borrow (i.e., acquirep) it. We must
// disable preemption to ensure it is not stolen, which would make the
// caller lose ownership.
acquirem()
gp := getg()
if gp.m.p == 0 {
acquirep(pp) // temporarily borrow p for mallocs in this function
}
mp := new(m)
mp.mstartfn = fn
mcommoninit(mp, id)
mp.g0.m = mp
releasem(gp.m)
allocmLock.runlock()
return mp
}Sonra runtime.newm1 gerçek sistem iş parçacığı oluşturmayı tamamlamak için runtime.newosproc fonksiyonunu çağırır.
func newm1(mp *m) {
execLock.rlock()
newosproc(mp)
execLock.runlock()
}runtime.newosproc uygulaması işletim sistemine göre değişir. Tam olarak nasıl oluşturulduğu bizim endişemiz değildir; bu işletim sistemi tarafından işlenir. Sonra M'nin çalışmasını başlatmak için runtime.mstart kullanılır.
Çıkış
runtime.gogo(&mp.g0.sched)Başlatma sırasında belirtildiği gibi, mstart1 fonksiyonunu çağırırken, yürütme bağlamı g0'ın sched alanında kaydedilir. Bu alanı runtime.gogo fonksiyonuna (assembly ile uygulanır) geçirmek, iş parçacığının yürütme bağlamına atlamasını ve yürütmeye devam etmesini sağlar. Kaydederken, getcallerpc() kullanıldı, bu yüzden bağlam geri yüklendiğinde, mstart0 fonksiyonuna döner.
mstart1()
if mStackIsSystemAllocated() {
osStack = true
}
mexit(osStack)Yürütme bağlamı geri yüklendikten sonra, yürütme sırasını takip ederek, iş parçacığından çıkmak için mexit fonksiyonuna girer.
mp := getg().m
unminit()
lock(&sched.lock)
for pprev := &allm; *pprev != nil; pprev = &(*pprev).alllink {
if *pprev == mp {
*pprev = mp.alllink
}
}
mp.freeWait.Store(freeMWait)
mp.freelink = sched.freem
sched.freem = mp
unlock(&sched.lock)
handoffp(releasep())
mdestroy(mp)
exitThread(&mp.freeWait)Esas olarak aşağıdaki şeyleri yapar:
runtime.unminitçağırarakruntime.minit'in işini geri al- Bu M'yi global değişken
allm'den kaldır - Zamanlayıcının
freem'inin mevcut M'yi göstermesini ayarla runtime.releasepkullanarak P'yi mevcut M'den ayır veruntime.handoffpkullanarak P'yi diğer M ile bağlayarak çalışmaya devam etruntime.destroykullanarak M'nin kaynaklarını yok et- Son olarak, işletim sistemi iş parçacığından çıkar
Bu noktada, M başarıyla çıktı.
Duraklatma
M zamanlayıcı zamanlaması, GC, sistem çağrıları veya diğer nedenler nedeniyle duraklatılması gerektiğinde, iş parçacığını duraklatmak için runtime.stopm fonksiyonu çağırılır. Aşağıda basitleştirilmiş kod bulunmaktadır.
func stopm() {
gp := getg()
lock(&sched.lock)
mput(gp.m)
unlock(&sched.lock)
mPark()
acquirep(gp.m.nextp.ptr())
gp.m.nextp = 0
}İlk olarak M'yi global boş M listesine koyar, sonra mPark() kullanarak mevcut iş parçacığını notesleep(&gp.m.park)'ta engeller. Uyandırıldığında, bu fonksiyon döner.
func mPark() {
gp := getg()
notesleep(&gp.m.park)
noteclear(&gp.m.park)
}Uyandırıldıktan sonra, M bağlanmak için bir P arayacak ve görevleri yürütmeye devam edecek.
Coroutine'ler
Bir coroutine'in yaşam döngüsü tam olarak bir coroutine'in birkaç durumuna karşılık gelir. Coroutine yaşam döngüsünü anlamak zamanlayıcıyı anlamak için çok yardımcıdır, sonuçta tüm zamanlayıcı coroutine'ler etrafında tasarlanmıştır. Tüm coroutine yaşam döngüsü aşağıdaki şekilde gösterildiği gibidir.

_Gcopystack coroutine yığını genişlediğindeki durumdur, Coroutine Yığını bölümünde açıklanacaktır.
Oluşturma
Sözdizimi açısından, coroutine oluşturma sadece bir go anahtar kelimesi artı bir fonksiyon gerektirir.
go doSomething()Derlemeden sonra, runtime.newproc fonksiyonuna bir çağrı olur:
func newproc(fn *funcval) {
gp := getg()
pc := getcallerpc()
systemstack(func() {
newg := newproc1(fn, gp, pc)
pp := getg().m.p.ptr()
runqput(pp, newg, true)
if mainStarted {
wakep()
}
})
}Gerçek oluşturma runtime.newproc1 tarafından tamamlanır. Oluşturma sırasında, ilk olarak M'yi kilitler, önlemeyi yasaklar, sonra P'nin yerel gfree listesindeki boş G'leri yeniden kullanmak için arar. Bulunamazsa, runtime.malg kullanarak yeni bir G oluşturur ve 2kb yığın alanı tahsis eder. Bu noktada, G'nin durumu _Gdead'dir.
mp := acquirem() // disable preemption because we hold M and P in local vars.
pp := mp.p.ptr()
newg := gfget(pp)
if newg == nil {
newg = malg(stackMin)
casgstatus(newg, _Gidle, _Gdead)
allgadd(newg) // publishes with a g->status of Gdead so GC scanner doesn't look at uninitialized stack.
}Go 1.18 ve sonrasında, parametre kopyalama artık newproc1 fonksiyonu tarafından tamamlanmaz. Bundan önce, runtime.memmove fonksiyon parametrelerini kopyalamak için kullanılırdı. Şimdi sadece coroutine'in yığın alanını sıfırlamaktan sorumludur, coroutine çıkış işleme için yığın tabanı olarak runtime.goexit kullanır, sonra giriş fonksiyonunun PC'sini ayarlar newg.startpc = fn.fn yürütmenin buradan başlayacağını gösterir. Ayarlama tamamlandıktan sonra, G'nin durumu _Grunnable'dır.
totalSize := uintptr(4*goarch.PtrSize + sys.MinFrameSize) // extra space in case of reads slightly beyond frame
totalSize = alignUp(totalSize, sys.StackAlign)
sp := newg.stack.hi - totalSize
spArg := sp
if usesLR {
// caller's LR
*(*uintptr)(unsafe.Pointer(sp)) = 0
prepGoExitFrame(sp)
spArg += sys.MinFrameSize
}
memclrNoHeapPointers(unsafe.Pointer(&newg.sched), unsafe.Sizeof(newg.sched))
newg.sched.sp = sp
newg.stktopsp = sp
newg.sched.pc = abi.FuncPCABI0(goexit) + sys.PCQuantum // +PCQuantum so that previous instruction is in same function
newg.sched.g = guintptr(unsafe.Pointer(newg))
gostartcallfn(&newg.sched, fn)
newg.parentGoid = callergp.goid
newg.gopc = callerpc
newg.ancestors = saveAncestors(callergp)
newg.startpc = fn.fn
casgstatus(newg, _Gdead, _Grunnable)Son olarak, G'nin benzersiz tanımlayıcısını ayarlayın, sonra M'yi bırakın ve oluşturulan coroutine G'yi döndürün.
newg.goid = pp.goidcache
pp.goidcache++
releasem(mp)
return newgCoroutine oluşturulduktan sonra, runtime.runqput fonksiyonu onu P'nin yerel kuyruğuna koymaya çalışır. Sığmazsa, global kuyruğa yerleştirilir. Tüm coroutine oluşturma süreci sırasında, durumu önce _Gidle'dan _Gdead'e değişir ve giriş fonksiyonu ayarlandıktan sonra, _Gdead'den _Grunnable'a değişir.
Çıkış
Oluşturma sırasında, Go zaten runtime.goexit fonksiyonunu coroutine'in yığın tabanı olarak ayarlamıştır. Bu yüzden coroutine yürütmesi tamamlandığında, sonunda bu fonksiyona girer. goexit->goexit1->goexit0 çağrı zinciri aracılığıyla, runtime.goexit0 fonksiyonu nihai olarak coroutine çıkış işinden sorumludur.
func goexit0(gp *g) {
mp := getg().m
pp := mp.p.ptr()
...
casgstatus(gp, _Grunning, _Gdead)
...
gp.m = nil
locked := gp.lockedm != 0
gp.lockedm = 0
mp.lockedg = 0
gp.preemptStop = false
gp.paniconfault = false
gp._defer = nil // should be true already but just in case.
gp._panic = nil // non-nil for Goexit during panic. points at stack-allocated data.
gp.writebuf = nil
gp.waitreason = waitReasonZero
gp.param = nil
gp.labels = nil
gp.timer = nil
dropg()
...
gfput(pp, gp)
...
schedule()
}Bu fonksiyon esas olarak aşağıdaki şeyleri yapar:
- Durumu
_Gdeadolarak ayarla - Alan değerlerini sıfırla
dropg()M ve G arasındaki ilişkiyi kesergfput(pp, gp)mevcut G'yi P'nin yerel boş listesine koyarschedule()yeni bir zamanlama turu yapar, M'nin yürütme haklarını diğer G'ye verir
Çıktıktan sonra, coroutine'in durumu _Grunning'den _Gdead'e değişir ve gelecekte yeni coroutine'ler oluşturulduğunda hala yeniden kullanılabilir.
Sistem Çağrıları
Coroutine G kullanıcı kodunu yürütürken, bir sistem çağrısı yaparsa, bir sistem çağrısını tetiklemenin iki yolu vardır:
syscallstandart kütüphanesinden sistem çağrıları- cgo çağrıları
Sistem çağrıları işçi iş parçacıklarını engellediği için, öncesinde hazırlık çalışması yapılması gerekir, bu runtime.entersyscall fonksiyonu tarafından tamamlanır. Ancak ilki sadece basit bir runtime.reentersyscall fonksiyon çağrısıdır ve gerçek iş ikincisi tarafından tamamlanır. İlk olarak, mevcut M'yi kilitler. Hazırlık sırasında, G'nin önlenmesi yasaktır ve yığın genişlemesi yasaktır. gp.stackguard0 = stackPreempt ayarlaması hazırlık tamamlandıktan sonra, P'nin yürütme haklarının diğer G tarafından önleneceğini gösterir. Sonra sistem çağrısı döndükten sonra kurtarma için coroutine'in yürütme bağlamını kaydeder.
gp := getg()
// Disable preemption because during this function g is in Gsyscall status,
// but can have inconsistent g->sched, do not let GC observe it.
gp.m.locks++
// Entersyscall must not call any function that might split/grow the stack.
// (See details in comment above.)
// Catch calls that might, by replacing the stack guard with something that
// will trip any stack check and leaving a flag to tell newstack to die.
gp.stackguard0 = stackPreempt
gp.throwsplit = true
// Leave SP around for GC and traceback.
save(pc, sp)
gp.syscallsp = sp
gp.syscallpc = pcOndan sonra, uzun süreli engelleme diğer G'nin yürütmesini etkilememesi için, M ve P'nin bağları çözülür. Bağlantı kesildikten sonra, M ve G sistem çağrılarını yürüttüğü için engellenecek, P ise bağlantı kesildikten sonra diğer boş M ile bağlanabilir, böylece P'nin yerel kuyruğundaki diğer G çalışmaya devam edebilir.
casgstatus(gp, _Grunning, _Gsyscall)
gp.m.syscalltick = gp.m.p.ptr().syscalltick
pp := gp.m.p.ptr()
pp.m = 0
gp.m.oldp.set(pp)
gp.m.p = 0
atomic.Store(&pp.status, _Psyscall)
gp.m.locks--Hazırlık tamamlandıktan sonra, M'nin kilidini bırakın. Bu süre zarfında, G'nin durumu _Grunning'den _Gsyscall'a değişir ve P'nin durumu _Psyscall olur.
Sistem çağrısı döndüğünde, iş parçacığı M artık engellenmez ve karşılık gelen G de kullanıcı kodunu yürütmek için tekrar zamanlanmalıdır, bu runtime.exitsyscall fonksiyonu tarafından tamamlanır. İlk olarak, mevcut M'yi kilitler ve eski P'ye bir referans alır.
gp := getg()
gp.waitsince = 0
oldp := gp.m.oldp.ptr()
gp.m.oldp = 0Bu noktada, işlenmesi gereken iki durum vardır. İlk durum doğrudan kullanım için mevcut bir P olup olmadığıdır. runtime.exitsyscallfast fonksiyonu orijinal P'nin mevcut olup olmadığını belirler, yani P'nin durumunun _Psyscall olup olmadığı. Aksi takdirde, boş bir P arayacaktır.
func exitsyscallfast(oldp *p) bool {
gp := getg()
// Freezetheworld sets stopwait but does not retake P's.
if sched.stopwait == freezeStopWait {
return false
}
// Try to re-acquire the last P.
if oldp != nil && oldp.status == _Psyscall && atomic.Cas(&oldp.status, _Psyscall, _Pidle) {
// There's a cpu for us, so we can run.
wirep(oldp)
exitsyscallfast_reacquired()
return true
}
// Try to get any other idle P.
if sched.pidle != 0 {
var ok bool
systemstack(func() {
ok = exitsyscallfast_pidle()
})
if ok {
return true
}
}
return false
}Kullanılabilir bir P başarıyla bulunursa, M P ile bağlanır, G _Gsyscall durumundan _Grunning durumuna geçer ve sonra runtime.Gosched aracılığıyla, G gönüllü olarak yürütme haklarını bırakır ve P diğer mevcut G'yi aramak için zamanlama döngüsüne girer.
oldp := gp.m.oldp.ptr()
gp.m.oldp = 0
if exitsyscallfast(oldp) {
// There's a cpu for us, so we can run.
gp.m.p.ptr().syscalltick++
// We need to cas the status and scan before resuming...
casgstatus(gp, _Gsyscall, _Grunning)
// Garbage collector isn't running (since we are),
// so okay to clear syscallsp.
gp.syscallsp = 0
gp.m.locks--
if gp.preempt {
// restore the preemption request in case we've cleared it in newstack
gp.stackguard0 = stackPreempt
} else {
// otherwise restore the real stackGuard, we've spoiled it in entersyscall/entersyscallblock
gp.stackguard0 = gp.stack.lo + stackGuard
}
gp.throwsplit = false
if sched.disable.user && !schedEnabled(gp) {
// Scheduling of this goroutine is disabled.
Gosched()
}
return
}Bulunamazsa, M G ile bağlantısını keser, G _Gsyscall'dan _Grunnable durumuna geçer, sonra boş bir P bulmak için tekrar dener. Bulunamazsa, doğrudan G'yi global kuyruğa koyar, sonra yeni bir zamanlama döngüsüne girer. Eski M runtime.stopm aracılığıyla boş duruma girer, gelecekte yeni görevleri bekler. Eğer P bulunursa, eski M ve G yeni P ile ilişkilendirilir, sonra kullanıcı kodunu yürütmeye devam eder, durum _Grunnable'dan _Grunning'e değişir.
func exitsyscall0(gp *g) {
casgstatus(gp, _Gsyscall, _Grunnable)
dropg()
lock(&sched.lock)
var pp *p
if schedEnabled(gp) {
pp, _ = pidleget(0)
}
var locked bool
if pp == nil {
globrunqput(gp)
}
unlock(&sched.lock)
if pp != nil {
acquirep(pp)
execute(gp, false) // Never returns.
}
stopm()
schedule() // Never returns.
}Sistem çağrısından çıktıktan sonra, G'nin durumu nihai olarak iki sonuca sahiptir: biri _Grunnable zamanlanmayı bekler ve diğeri _Grunning çalışmaya devam eder.
Askıya Alma
Mevcut coroutine bir nedenle askıya alındığında, durumu _Grunnable'dan _Gwaiting'e değişir. Askıya almanın birçok nedeni vardır, örneğin kanal engelleme, select, kilitler veya time.sleep. Daha fazla neden için G Yapısı'na bakın. time.Sleep örneğini alırsak, aslında runtime.timesleep'e bağlanır. İkincinin kodu şöyledir.
func timeSleep(ns int64) {
if ns <= 0 {
return
}
gp := getg()
t := gp.timer
if t == nil {
t = new(timer)
gp.timer = t
}
t.f = goroutineReady
t.arg = gp
t.nextwhen = nanotime() + ns
if t.nextwhen < 0 { // check for overflow.
t.nextwhen = maxWhen
}
gopark(resetForSleep, unsafe.Pointer(t), waitReasonSleep, traceBlockSleep, 1)
}Gördüğünüz gibi, getg aracılığıyla mevcut coroutine'i alır, sonra runtime.gopark kullanarak mevcut coroutine'i askıya alır. runtime.gopark G ve M'nin engelleme nedenini günceller, M'nin kilidini bırakır.
mp := acquirem()
gp := mp.curg
status := readgstatus(gp)
if status != _Grunning && status != _Gscanrunning {
throw("gopark: bad g status")
}
mp.waitlock = lock
mp.waitunlockf = unlockf
gp.waitreason = reason
mp.waitTraceBlockReason = traceReason
mp.waitTraceSkip = traceskip
releasem(mp)
// can't do anything that might move the G between Ms here.
mcall(park_m)Sonra sistem yığınına geçer ve G'nin durumunu _Gwaiting'e değiştirmek için runtime.park_m kullanır, sonra M ve G arasındaki ilişkiyi keser ve yürütme haklarını diğer G'ye vermek için yeni bir zamanlama döngüsüne girer. Askıya alındıktan sonra, G ne kullanıcı kodunu yürütür ne de yerel kuyruktadır, sadece M ve P'ye referansları korur.
mp := getg().m
casgstatus(gp, _Grunning, _Gwaiting)
dropg()
schedule()runtime.timesleep fonksiyonunda, t.f değerini belirten bu satır kod vardır:
t.f = goroutineReadyBu runtime.goroutineReady fonksiyonu askıya alınmış coroutine'leri uyandırmak için kullanılır. Coroutine'i uyandırmak için runtime.ready fonksiyonunu çağırır.
status := readgstatus(gp)
// Mark runnable.
mp := acquirem()
casgstatus(gp, _Gwaiting, _Grunnable)
runqput(mp.p.ptr(), gp, next)
wakep()
releasem(mp)Uyandırdıktan sonra, G'nin durumunu _Grunnable'a değiştirin, sonra gelecekteki zamanlama için beklemek üzere G'yi P'nin yerel kuyruğuna koyun.
Coroutine Yığını
Go dilindeki coroutine'ler tipik bir yığın tabanlı coroutine'dir. Her coroutine yığında bağımsız bir yığın alanı tahsis edilir ve kullanım değişiklikleriyle büyür veya küçülür. Zamanlayıcı başlatma sırasında, runtime.stackinit fonksiyonu global yığın alanı önbelleği stackpool ve stackLarge'ı başlatmaktan sorumludur.
func stackinit() {
if _StackCacheSize&_PageMask != 0 {
throw("cache size must be a multiple of page size")
}
for i := range stackpool {
stackpool[i].item.span.init()
lockInit(&stackpool[i].item.mu, lockRankStackpool)
}
for i := range stackLarge.free {
stackLarge.free[i].init()
lockInit(&stackLarge.lock, lockRankStackLarge)
}
}Ayrıca, her P'nin kendi bağımsız yığın alanı önbelleği mcache vardır:
type p struct {
...
mcache *mcache
...
}
type mcache struct {
_ sys.NotInHeap
nextSample uintptr
scanAlloc uintptr
tiny uintptr
tinyoffset uintptr
tinyAllocs uintptr
alloc [numSpanClasses]*mspan
stackcache [_NumStackOrders]stackfreelist
flushGen atomic.Uint32
}İş parçacığı önbelleği mcache her iş parçacığı için bağımsızdır ve yığın belleğinde tahsis edilmez. Erişirken kilitleme gerekmez. Bu üç yığın önbelleği sonraki alan tahsisinde kullanılacaktır.
Tahsis
Bir coroutine oluştururken, yeniden kullanılabilir bir coroutine yoksa, onun için yeni bir yığın alanı tahsis edilir. Varsayılan boyutu 2KB'dir.
newg := gfget(pp)
if newg == nil {
newg = malg(stackMin)
casgstatus(newg, _Gidle, _Gdead)
allgadd(newg) // publishes with a g->status of Gdead so GC scanner doesn't look at uninitialized stack.
}Yığın alanını tahsis etmekten sorumlu fonksiyon runtime.stackalloc'tır:
func stackalloc(n uint32) stackİstenen yığın bellek boyutunun 32KB'den az olup olmadığına göre iki durum vardır. 32KB aynı zamanda Go'nun bir nesnenin küçük mü büyük mü olduğunu yargılaması için standardıdır. Bu değerden azsa, stackpool önbelleğinden alınır. M P ile bağlı olduğunda ve M'nin önlenmesine izin verilmediğinde, yerel iş parçacığı önbelleğinden alınır.
if n < fixedStack<<_NumStackOrders && n < _StackCacheSize {
order := uint8(0)
n2 := n
for n2 > fixedStack {
order++
n2 >>= 1
}
var x gclinkptr
if stackNoCache != 0 || thisg.m.p == 0 || thisg.m.preemptoff != "" {
lock(&stackpool[order].item.mu)
x = stackpoolalloc(order)
unlock(&stackpool[order].item.mu)
} else {
c := thisg.m.p.ptr().mcache
x = c.stackcache[order].list
if x.ptr() == nil {
stackcacherefill(c, order)
x = c.stackcache[order].list
}
c.stackcache[order].list = x.ptr().next
c.stackcache[order].size -= uintptr(n)
}
v = unsafe.Pointer(x)
}32KB'den büyükse, stackLarge önbelleğinden alınır. Eğer hala yeterli değilse, bellek doğrudan yığında tahsis edilir.
else {
var s *mspan
npage := uintptr(n) >> _PageShift
log2npage := stacklog2(npage)
// Try to get a stack from the large stack cache.
lock(&stackLarge.lock)
if !stackLarge.free[log2npage].isEmpty() {
s = stackLarge.free[log2npage].first
stackLarge.free[log2npage].remove(s)
}
unlock(&stackLarge.lock)
lockWithRankMayAcquire(&mheap_.lock, lockRankMheap)
if s == nil {
// Allocate a new stack from the heap.
s = mheap_.allocManual(npage, spanAllocStack)
if s == nil {
throw("out of memory")
}
osStackAlloc(s)
s.elemsize = uintptr(n)
}
v = unsafe.Pointer(s.base())
}Tamamlandıktan sonra, yığın alanının düşük ve yüksek adreslerini döndürün:
return stack{uintptr(v), uintptr(v) + uintptr(n)}Genişleme
Varsayılan coroutine yığın boyutu 2KB'dir, bu yeterince hafiftir, bu yüzden bir coroutine oluşturma maliyeti çok düşüktür. Ancak bu yeterli olmayabilir. Yığın alanı yeterli olmadığında, genişlemesi gerekir. Derleyici fonksiyonların başına runtime.morestack fonksiyonunu ekler, mevcut coroutine'in yığın genişletmesine ihtiyacı olup olmadığını kontrol eder. Eğer gerekirse, gerçek genişletme işlemini tamamlamak için runtime.newstack çağırır.
TIP
morestack neredeyse tüm fonksiyonların başına eklendiği için, yığın genişletme kontrol zamanı aynı zamanda bir coroutine önleme noktasıdır.
thisg := getg()
gp := thisg.m.curg
// Allocate a bigger segment and move the stack.
oldsize := gp.stack.hi - gp.stack.lo
newsize := oldsize * 2
// The goroutine must be executing in order to call newstack,
// so it must be Grunning (or Gscanrunning).
casgstatus(gp, _Grunning, _Gcopystack)
// The concurrent GC will not scan the stack while we are doing the copy since
// the gp is in a Gcopystack status.
copystack(gp, newsize)
casgstatus(gp, _Gcopystack, _Grunning)
gogo(&gp.sched)Gördüğünüz gibi, hesaplanan yığın alanı kapasitesi orijinalin iki katıdır. runtime.copystack fonksiyonu yığın kopyalama işini tamamlar. Kopyalamadan önce, G'nin durumu _Grunning'den _Gcopystack'e geçer.
func copystack(gp *g, newsize uintptr) {
old := gp.stack
used := old.hi - gp.sched.sp
// allocate new stack
new := stackalloc(uint32(newsize))
// Compute adjustment.
var adjinfo adjustinfo
adjinfo.old = old
adjinfo.delta = new.hi - old.hi
// Copy the stack (or the rest of it) to the new location
memmove(unsafe.Pointer(new.hi-ncopy), unsafe.Pointer(old.hi-ncopy), ncopy)
// Adjust remaining structures that have pointers into stacks.
// We have to do most of these before we traceback the new
// stack because gentraceback uses them.
adjustctxt(gp, &adjinfo)
adjustdefers(gp, &adjinfo)
adjustpanics(gp, &adjinfo)
if adjinfo.sghi != 0 {
adjinfo.sghi += adjinfo.delta
}
// Swap out old stack for new one
gp.stack = new
gp.stackguard0 = new.lo + stackGuard // NOTE: might clobber a preempt request
gp.sched.sp = new.hi - used
gp.stktopsp += adjinfo.delta
// Adjust pointers in the new stack.
var u unwinder
for u.init(gp, 0); u.valid(); u.next() {
adjustframe(&u.frame, &adjinfo)
}
stackfree(old)
}Bu fonksiyon aşağıdaki işleri yapar:
- Yeni yığın alanı tahsis et
- Eski yığın belleğini doğrudan
runtime.memmovearacılığıyla yeni yığın alanına kopyala - Yığın işaretçileri içeren yapıları ayarla, örneğin defer, panic, vb.
- G'nin yığın alanı alanını güncelle
- Eski yığın belleğine işaret eden işaretçileri
runtime.adjustframearacılığıyla ayarla - Eski yığın belleğini bırak
Tamamlandıktan sonra, G'nin durumu _Gcopystack'tan _Grunning'e geçer ve runtime.gogo fonksiyonu G'nin kullanıcı kodunu yürütmeye devam etmesini sağlar. Go'da belleğin istikrarsız olmasının nedeni tam olarak coroutine yığın genişlemesidir.
Daraltma
G'nin durumu _Grunnable, _Gsyscall veya _Gwaiting olduğunda, GC coroutine yığınının bellek alanını tarar.
func scanstack(gp *g, gcw *gcWork) int64 {
switch readgstatus(gp) &^ _Gscan {
case _Grunnable, _Gsyscall, _Gwaiting:
// ok
}
...
if isShrinkStackSafe(gp) {
// Shrink the stack if not much of it is being used.
shrinkstack(gp)
}
...
}Gerçek yığın daraltma işi runtime.shrinkstack tarafından tamamlanır.
func shrinkstack(gp *g) {
if !isShrinkStackSafe(gp) {
throw("shrinkstack at bad time")
}
oldsize := gp.stack.hi - gp.stack.lo
newsize := oldsize / 2
if newsize < fixedStack {
return
}
avail := gp.stack.hi - gp.stack.lo
if used := gp.stack.hi - gp.sched.sp + stackNosplit; used >= avail/4 {
return
}
copystack(gp, newsize)
}Kullanılan yığın alanı orijinalin 1/4'ünden az olduğunda, orijinalin 1/2'sine daraltmak için runtime.copystack kullanır. Kalan iş öncekiyle aynıdır.
Bölümlü Yığın
copystack sürecinden görebileceğiniz gibi, eski yığın belleğini daha büyük bir yığın alanına kopyalar. Hem orijinal yığın hem de yeni yığın sürekli bellek adreslerine sahiptir. Antik Go dilinde, yığın genişlemesi farklı yapılırdı. O zamanlar, bellek kopyalamanın çok performans tüketici olduğu düşünülüyordu, bu yüzden bölümlü yığın yaklaşımı benimsendi. Eğer yığın alanı belleği yeterli değilse, yeni bir yığın alanı için başvurulurdu. Orijinal yığın alanı belleği bırakılmaz veya kopyalanmazdı ve işaretçiler aracılığıyla birbirine bağlanırdı, bir yığın bağlı listesi oluştururdu. Bu bölümlü yığınların kökenidir, aşağıdaki şekilde gösterildiği gibi.

Bu yaklaşımın avantajı orijinal yığını kopyalamaya gerek olmamasıdır, ancak dezavantajları da çok belirgindir: yığın genişlemesi ve daraltmasını çok sık tetikler. Yığın alanı boş belleği azaldığında, yeni fonksiyon çağrıları yığın genişlemesini tetikler. Bu fonksiyonlar döndüğünde ve yeni yığın alanı artık gerekli olmadığında, tekrar daraltma tetikler. Eğer bu fonksiyon çağrıları çok sık ise, bu tür işlemlerin neden olduğu performans kaybı çok önemlidir.
Bu yüzden Go 1.4'ten sonra, sürekli yığınlara geçildi. Sürekli yığınlar daha büyük kapasiteli bir yığın alanı tahsis eder, bu yüzden kullanılan bellek kritik bir değere ulaştığında, fonksiyon çağrıları nedeniyle sık sık genişleme/daraltma tetiklemez. Ve bellek adresleri sürekli olduğu için, önbellek uzamsal yerellik ilkesine göre, sürekli yığınlar CPU önbelleği için daha dostanedir.
Zamanlama Döngüsü

Zamanlayıcı başlatma bölümünde belirtildiği gibi, runtime.mstart1 fonksiyonunda, M ve P başarıyla ilişkilendirildikten sonra, ilk runtime.schedule zamanlama döngüsüne girer, resmi olarak G'yi kullanıcı kodunu yürütmek için zamanlamaya başlar. Zamanlama döngüsünde, bu kısım principalmente P'nin rol oynadığı yerdir. M sistem iş parçacıklarına karşılık gelir, G giriş fonksiyonlarına (yani kullanıcı koduna) karşılık gelir, ancak P M ve G gibi karşılık gelen varlıklara sahip değildir. Sadece soyut bir kavramdır, M ve G arasındaki ilişkiyi işleyen bir aracı olarak hareket eder.
func schedule() {
mp := getg().m
top:
pp := mp.p.ptr()
pp.preempt = false
if mp.spinning {
resetspinning()
}
gp, inheritTime, tryWakeP := findRunnable() // blocks until work is available
execute(gp, inheritTime)
}Yukarıdaki kod basitleştirilmiştir, birçok koşullu yargı kaldırılmıştır. En çekirdek noktalar sadece ikidir: runtime.findRunnable ve runtime.execute. İlki bir G bulmaktan sorumludur ve kesinlikle mevcut bir G döndürür, ikincisi ise G'nin kullanıcı kodunu yürütmeye devam etmesinden sorumludur.
findRunnable fonksiyonu için, ilk G kaynağı P'nin yerel kuyruğudur:
// local runq
if gp, inheritTime := runqget(pp); gp != nil {
return gp, inheritTime, false
}Yerel kuyrukta G yoksa, sonra global kuyruktan almaya çalışın:
// global runq
if sched.runqsize != 0 {
lock(&sched.lock)
gp := globrunqget(pp, 0)
unlock(&sched.lock)
if gp != nil {
return gp, false, false
}
}Yerel ve global kuyruklarda bulunamazsa, ağ poller'dan almaya çalışın:
if netpollinited() && netpollWaiters.Load() > 0 && sched.lastpoll.Load() != 0 {
if list := netpoll(0); !list.empty() { // non-blocking
gp := list.pop()
injectglist(&list)
casgstatus(gp, _Gwaiting, _Grunnable)
if traceEnabled() {
traceGoUnpark(gp, 0)
}
return gp, false, false
}
}Hala bulunamazsa, nihayetinde diğer P'nin yerel kuyruklarından G çalar. Coroutine oluşturma sırasında belirtildiği gibi, P'nin yerel kuyruğundaki G'nin bir ana kaynağı mevcut coroutine tarafından oluşturulan alt coroutine'lerdir. Ancak, tüm coroutine'ler alt coroutine'ler oluşturmaz, bu yüzden bazı P'nin çok meşgul olması ve diğer P'nin boş olması mümkündür. Bu, bazı G'nin uzun süre çalıştırılamayacak şekilde beklemesine ve diğer tarafta P'nin yapacak bir şeyi olmamasına neden olabilir. Tüm P'yi sıkmak ve iş verimliliğini maksimize etmek için, P G bulamadığında, diğer P'nin yerel kuyruklarından yürütülebilir G "çalar". Bu şekilde, her P nispeten düzgün bir G kuyruğuna sahip olabilir ve P'nin birbirine nehir karşısından bakması durumu daha az olasıdır.
gp, inheritTime, tnow, w, newWork := stealWork(now)
if gp != nil {
// Successfully stole.
return gp, inheritTime, false
}runtime.stealWork rastgele bir P seçer ve ondan çalar. Gerçek çalma işi runtime.runqgrab fonksiyonu tarafından tamamlanır, bu o P'nin yerel kuyruğundan G'nin yarısını çalmaya çalışır.
for {
h := atomic.LoadAcq(&pp.runqhead) // load-acquire, synchronize with other consumers
t := atomic.LoadAcq(&pp.runqtail) // load-acquire, synchronize with the producer
n := t - h
n = n - n/2
if n > uint32(len(pp.runq)/2) { // read inconsistent h and t
continue
}
for i := uint32(0); i < n; i++ {
g := pp.runq[(h+i)%uint32(len(pp.runq))]
batch[(batchHead+i)%uint32(len(batch))] = g
}
if atomic.CasRel(&pp.runqhead, h, h+n) { // cas-release, commits consume
return n
}
}Tüm çalma işi dört kez yapılır. Eğer dört kez sonra hiç G çalınamazsa, döner. Eğer nihayetinde bulunamazsa, mevcut M runtime.stopm tarafından duraklatılır, yukarıdaki adımları tekrarlamak için uyandırılana kadar. Bir G bulunduğunda ve döndürüldüğünde, G'yi çalıştırmak için runtime.execute'a devredilir.
mp := getg().m
mp.curg = gp
gp.m = mp
casgstatus(gp, _Grunnable, _Grunning)
gp.waitsince = 0
gp.preempt = false
gp.stackguard0 = gp.stack.lo + stackGuard
gogo(&gp.sched)İlk olarak M'nin curg'ını güncelleyin, sonra G'nin durumunu _Grunning'e güncelleyin ve son olarak G'nin yürütmesini geri yüklemek için runtime.gogo'ya devredin.
Genel olarak, zamanlama döngüsünde, G kaynakları önceliğe göre dört seviyeye ayrılır:
- P'nin yerel kuyruğu
- Global kuyruk
- Ağ poller
- Diğer P'nin yerel kuyruklarından çal
runtime.execute yürütüldükten sonra dönmez. Az önce elde edilen G sonsuza kadar yürütmez ya. Zamanlamanın tetiklendiği bir noktada, yürütme hakları elinden alınır, sonra yeni bir zamanlama döngüsüne girer ve yürütme haklarını diğer G'ye verir.
Zamanlama Stratejisi
Farklı G'nin kullanıcı kodu için farklı yürütme süreleri olabilir. Bazı G uzun zaman alabilir, diğerleri kısa zaman alır. Uzun yürütme süreli G diğer G'nin uzun süre yürütülememesine neden olabilir, bu yüzden G'nin alternatif yürütmesi doğru yoldur. Bu çalışma yöntemi işletim sistemlerinde eşzamanlılık olarak adlandırılır.
İşbirlikçi Zamanlama
İşbirlikçi zamanlamanın temel fikri G'nin yürütme haklarını gönüllü olarak diğer G'ye bırakmasıdır. Esas olarak iki yöntem vardır.
İlk yöntem kullanıcı kodunda gönüllü olarak bırakmaktır. Go runtime.Gosched() fonksiyonunu sağlar, kullanıcıların ne zaman yürütme haklarını bırakacaklarına karar vermesini sağlar. Ancak, birçok durumda, zamanlayıcının iç çalışma detayları kullanıcılar için bir kara kutudur, ne zaman gönüllü olarak bırakılacağını yargılamak zordur. Bu kullanıcılardan daha yüksek talepler gerektirir. Dahası, Go'nun zamanlayıcısı çoğu detayı kullanıcılardan gizlemeye ve daha basit kullanım yöntemleri peşinde koşmaya çalışır. Bu durumda, kullanıcıları zamanlama işine dahil etmek iyi bir şey değildir.
İkinci yöntem önleme işaretlemedir. Adında "önleme" kelimesi olsa da, esasen hala bir işbirlikçi zamanlama stratejisidir. Fikir, fonksiyonların başına önleme algılama kodu runtime.morestack() eklemektir. Ekleme süreci derleme aşamasında tamamlanır. Daha önce belirtildiği gibi, originally yığın genişletme algılama için bir fonksiyondu. Algılama noktası her fonksiyon çağrısı olduğu için, bu aynı zamanda önleme algılama için iyi bir zamandır. runtime.newstack fonksiyonunun üst kısmı önleme algılama içindir, alt kısmı ise yığın genişletme algılama içindir. Daha önce karışıklığı önlemek için, bu kısım atlandı. Şimdi bu kısmın ne yaptığını görelim. İlk olarak, gp.stackguard0'a göre bir önleme yargısı yapar. Önleme gerekmiyorsa, kullanıcı kodunu yürütmeye devam eder.
stackguard0 := atomic.Loaduintptr(&gp.stackguard0)
preempt := stackguard0 == stackPreempt
if preempt {
if !canPreemptM(thisg.m) {
gp.stackguard0 = gp.stack.lo + stackGuard
gogo(&gp.sched) // never return
}
}g.stackguard0 == stackPreempt olduğunda, runtime.canPreemptM() fonksiyonu coroutine koşullarının önlenmesi gerekip gerekmediğini yargılar. Kod şöyledir:
func canPreemptM(mp *m) bool {
return mp.locks == 0 && mp.mallocing == 0 && mp.preemptoff == "" && mp.p.ptr().status == _Prunning
}Gördüğünüz gibi, önlenebilir olmak dört koşulu karşılamayı gerektirir:
- M kilitli değil
- Şu anda bellek tahsis etmiyor
- Önleme devre dışı değil
- P
_Prunningdurumunda
Aşağıdaki iki durumda, g.stackguard0 stackPreempt olarak ayarlanır:
- Çöp toplama gerektiğinde
- Bir sistem çağrısı oluştuğunda
if preempt {
if gp.preemptShrink {
gp.preemptShrink = false
shrinkstack(gp)
}
// Act like goroutine called runtime.Gosched.
gopreempt_m(gp) // never return
}Sonunda, mevcut coroutine'in yürütme haklarını gönüllü olarak bırakmak için runtime.gopreempt_m()'e gider. İlk olarak, M ve G arasındaki bağlantıyı keser, durum _Grunnable olur, sonra G'yi global kuyruğa koyar ve son olarak yürütme haklarını diğer G'ye vermek için zamanlama döngüsüne girer.
casgstatus(gp, _Grunning, _Grunnable)
dropg()
lock(&sched.lock)
globrunqput(gp)
unlock(&sched.lock)
schedule()Bu şekilde, tüm coroutine'ler fonksiyon çağrıları sırasında önleme algılama için bu fonksiyona girebilir. Bu strateji fonksiyon çağrısı zamanlamasına önlemeyi tetiklemeye ve gönüllü olarak bırakmaya dayanır. Go 1.14'ten önce, Go her zaman bu zamanlama stratejisini kullanıyordu. Ancak bu bir soruna sahiptir: eğer fonksiyon çağrısı yoksa, algılama yapılamaz. Örneğin, aşağıdaki klasik kod, birçok eğitimde görünmüş olmalı:
func main() {
// Limit P quantity to only 1
runtime.GOMAXPROCS(1)
// Coroutine 1
go func() {
for {
// This coroutine keeps spinning idle
}
}()
// Enter system call, main coroutine yields to other coroutines
time.Sleep(time.Millisecond)
println("exit")
}Kod dönen boş bir coroutine 1 oluşturur, sonra ana coroutine sistem çağrısı nedeniyle gönüllü olarak bırakır. Bu noktada, coroutine 1 zamanlanıyor, ancak hiç fonksiyon çağrısı yapmadığı için önleme algılama yapamaz. Sadece bir P olduğu ve başka boş P olmadığı için, bu ana coroutine'in asla zamanlanmamasına ve exit'in asla çıktılanmamasına neden olur. Ancak, bu sorun Go 1.14'ten önce ile sınırlıdır.
Önleyici Zamanlama
Yetkililer Go 1.14'te sinyal tabanlı önleyici zamanlama stratejisi ekledi. Bu asenkron iş parçacıkları aracılığıyla sinyaller göndererek iş parçacıklarını önleyen asenkron bir önleme stratejisidir. Sinyal tabanlı önleyici zamanlamanın şu anda iki giriş noktası vardır: sistem izleme ve GC.
Sistem izleme döngüsünde, her P'yi tarar. Eğer P tarafından zamanlanan G 10ms'den fazla yürütürse, zorla önlemeyi tetikler. Bu iş runtime.retake fonksiyonu tarafından tamamlanır. Aşağıda basitleştirilmiş kod bulunmaktadır.
func retake(now int64) uint32 {
n := 0
lock(&allpLock)
for i := 0; i < len(allp); i++ {
pp := allp[i]
if pp == nil {
continue
}
pd := &pp.sysmontick
s := pp.status
sysretake := false
if s == _Prunning || s == _Psyscall {
// Preempt G if it's running for too long.
t := int64(pp.schedtick)
if int64(pd.schedtick) != t {
pd.schedtick = uint32(t)
pd.schedwhen = now
} else if pd.schedwhen+forcePreemptNS <= now {
preemptone(pp)
sysretake = true
}
}
}
unlock(&allpLock)
return uint32(n)
}Çöp toplama gerektiğinde, eğer G'nin durumu _Grunning ise, yani hala çalışıyor, önlemeyi de tetikler.
func suspendG(gp *g) suspendGState {
for i := 0; ; i++ {
switch s := readgstatus(gp); s {
case _Grunning:
gp.preemptStop = true
gp.preempt = true
gp.stackguard0 = stackPreempt
casfrom_Gscanstatus(gp, _Gscanrunning, _Grunning)
if preemptMSupported && debug.asyncpreemptoff == 0 && needAsync {
now := nanotime()
if now >= nextPreemptM {
nextPreemptM = now + yieldDelay/2
preemptM(asyncM)
}
}
......
......
func preemptM(mp *m) {
if mp.signalPending.CompareAndSwap(0, 1) {
if GOOS == "darwin" || GOOS == "ios" {
pendingPreemptSignals.Add(1)
}
signalM(mp, sigPreempt)
}
}Her iki önleme giriş noktası da nihayetinde runtime.preemptM fonksiyonuna girer, bu önleme sinyallerinin gönderilmesini tamamlar. Sinyal başarıyla gönderildiğinde, runtime.mstart'ta runtime.initsig aracılığıyla kaydedilen sinyal işleyici geri çağrı fonksiyonu runtime.sighandler devreye girer. Bir önleme sinyali gönderildiğini algılarsa, önlemeye başlar.
func sighandler(sig uint32, info *siginfo, ctxt unsafe.Pointer, gp *g) {
...
if sig == sigPreempt && debug.asyncpreemptoff == 0 && !delayedSignal {
// Might be a preemption signal.
doSigPreempt(gp, c)
}
...
}doSigPreempt hedef coroutine'in bağlamını değiştirir ve runtime.asyncPreempt çağrısını enjekte eder.
func doSigPreempt(gp *g, ctxt *sigctxt) {
// Check if this G wants to be preempted and is safe to
// preempt.
if wantAsyncPreempt(gp) {
if ok, newpc := isAsyncSafePoint(gp, ctxt.sigpc(), ctxt.sigsp(), ctxt.siglr()); ok {
// Adjust the PC and inject a call to asyncPreempt.
ctxt.pushCall(abi.FuncPCABI0(asyncPreempt), newpc)
}
}
...Bu şekilde, kullanıcı koduna geri dönüldüğünde, hedef coroutine runtime.asyncPreempt fonksiyonuna gider, bu runtime.asyncPreempt2 çağrısını içerir.
TEXT ·asyncPreempt(SB),NOSPLIT|NOFRAME,$0-0
PUSHQ BP
MOVQ SP, BP
// Save flags before clobbering them
PUSHFQ
// obj doesn't understand ADD/SUB on SP, but does understand ADJSP
ADJSP $368
// But vet doesn't know ADJSP, so suppress vet stack checking
...
CALL ·asyncPreempt2(SB)
...
RETMevcut coroutine'in çalışmayı durdurmasını ve yürütme haklarını diğer coroutine'lere vermek için yeni bir zamanlama döngüsü yapmasını sağlar.
func asyncPreempt2() {
gp := getg()
gp.asyncSafePoint = true
if gp.preemptStop {
mcall(preemptPark)
} else {
mcall(gopreempt_m)
}
gp.asyncSafePoint = false
}Tüm bu süreç runtime.asyncPreempt fonksiyonunda gerçekleşir, bu assembly ile uygulanır (runtime/preempt_*.s dosyasında bulunur) ve zamanlama tamamlandıktan sonra daha önce değiştirilen coroutine bağlamını geri yükler, böylece coroutine gelecekte normal olarak kurtarabilir. Asenkron önleme stratejisini benimsedikten sonra, önceki örnek artık ana coroutine'i kalıcı olarak engellemez. Dönen coroutine belirli bir süre çalıştığında, zamanlama döngüsünde zorla yürütülür, böylece yürütme haklarını ana coroutine'e verir, nihayetinde programın normal olarak sonlanmasını sağlar.
Özet
Genel olarak, zamanlamayı tetiklemek için zamanlama şunları içerir:
- Fonksiyon çağrıları
- Sistem çağrıları
- Sistem izleme
- Çöp toplama (GC çok uzun süre yürüten coroutine'leri de önler)
- Kanallar, kilitler veya diğer nedenler nedeniyle coroutine askıya alma
Zamanlama stratejileri principalmente iki kategoriye ayrılır: işbirlikçi ve önleyici. İşbirlikçi gönüllü olarak yürütme haklarını bırakmaktır, önleyici ise asenkron olarak yürütme haklarını ele geçirmektir. Her ikisi de bugünün zamanlayıcısını oluşturmak için bir arada bulunur.
