Skip to content

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.

go
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).

go
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:

go
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:

go
_panic   *_panic // innermost panic - offset known to liblink
_defer   *_defer // innermost defer

m şu anda bu g üzerinde yürüten coroutine'dir:

go
m        *m      // current m; offset known to arm liblink

preempt mevcut coroutine'in önlenmesi gerekip gerekmediğini gösterir, g.stackguard0 = stackpreempt ile eşdeğerdir:

go
preempt       bool // preemption signal, duplicates stackguard0 = stackpreempt

atomicstatus coroutine G'nin durum değerini saklamak için kullanılır, aşağıdaki opsiyonel değerlere sahiptir:

İsimAçıklama
_GidleYeni tahsis edildi ve henüz başlatılmadı
_GrunnableMevcut coroutine'in çalışabileceğini gösterir, bekleme kuyruğunda bulunur
_GrunningMevcut coroutine'in kullanıcı kodunu yürüttüğünü gösterir
_GsyscallSistem çağrılarını yürütmek için bir M atandı
_GwaitingCoroutine engellendi, engelleme nedeni aşağıya bakın
_GdeadMevcut coroutine'in kullanılmadığını gösterir, yeni çıkmış veya yeni başlatılmış olabilir
_GcopystackCoroutine yığını taşınıyor, bu süre zarfında kullanıcı kodu yürütülmez ve bekleme kuyruğunda değildir
_GpreemptedKendisi engellendi önlemeye girer, önleyici tarafından uyandırılmayı bekler
_GscanGC 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.

go
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.

go
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:

go
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 coroutine
  • curg: İşçi iş parçacığında çalışan kullanıcı coroutine'i
  • gsignal: İş parçacığı sinyallerini işlemekten sorumlu coroutine
  • goSigStack: Sinyal işleme için Go tarafından tahsis edilen yığın alanı
  • p: İşlemci P'nin adresi, oldp sistem çağrısı yürütmeden önce P'ye işaret eder, nextp yeni tahsis edilen P'ye işaret eder
  • mallocing: Şu anda yeni bellek alanı tahsis edilip edilmediğini gösterir
  • throwing: M oluştuğunda hata tipini gösterir
  • preemptoff: Önleme tanımlayıcısı, boş dize olduğunda şu anda çalışan coroutine'in önlenebileceğini gösterir
  • locks: Mevcut M'nin "kilit" sayısını gösterir, 0 olmadığında önleme yasaktır
  • dying: M'nin kurtarılamaz panic ile 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:

go
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ğerAçıklama
_PidleP boş durumda, zamanlayıcı tarafından M atanabilir veya diğer durumlar arasında geçiş yapıyor olabilir
_PrunningP M ile ilişkilendirilmiş ve kullanıcı kodunu yürütüyor
_PsyscallP 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
_PgcstopP'nin GC nedeniyle durduğunu gösterir
_PdeadP'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.

go
runqhead uint32
runqtail uint32
runq     [256]guintptr

runnext bir sonraki mevcut G'yi gösterir:

go
runnext guintptr

Diğer alanlar şu şekilde açıklanmıştır:

  • id: P'nin benzersiz tanımlayıcısı
  • schedtick: Her coroutine zamanlaması ile artar, runtime.execute fonksiyonunda görünür.
  • syscalltick: Her sistem çağrısı ile artar
  • sysmontick: Sistem izleyici tarafından son gözlemlenen bilgiyi kaydeder
  • m: P ile ilişkilendirilmiş M
  • gFree: Boş G listesi
  • preempt: 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.

go
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
  RET

Aş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.

go
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:

go
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:

  1. M'nin id'sini tahsis et
  2. İş parçacığı sinyallerini işlemek için ayrı bir G tahsis et, runtime.mpreinit fonksiyonu tarafından tamamlandı
  3. Global M bağlı listesinin runtime.allm baş 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.

go
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.

go
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:

go
// 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.

go
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.

go
// 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:

go
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 runnablePs

Ondan 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:

go
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.

go
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.

go
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.

go
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.

go
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ış

go
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.

go
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.

go
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:

  1. runtime.unminit çağırarak runtime.minit'in işini geri al
  2. Bu M'yi global değişken allm'den kaldır
  3. Zamanlayıcının freem'inin mevcut M'yi göstermesini ayarla
  4. runtime.releasep kullanarak P'yi mevcut M'den ayır ve runtime.handoffp kullanarak P'yi diğer M ile bağlayarak çalışmaya devam et
  5. runtime.destroy kullanarak M'nin kaynaklarını yok et
  6. 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.

go
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.

go
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
go doSomething()

Derlemeden sonra, runtime.newproc fonksiyonuna bir çağrı olur:

go
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.

go
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.

go
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.

go
newg.goid = pp.goidcache
pp.goidcache++
releasem(mp)

return newg

Coroutine 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.

go
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:

  1. Durumu _Gdead olarak ayarla
  2. Alan değerlerini sıfırla
  3. dropg() M ve G arasındaki ilişkiyi keser
  4. gfput(pp, gp) mevcut G'yi P'nin yerel boş listesine koyar
  5. schedule() 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:

  1. syscall standart kütüphanesinden sistem çağrıları
  2. 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.

go
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 = pc

Ondan 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.

go
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.

go
gp := getg()

gp.waitsince = 0
oldp := gp.m.oldp.ptr()
gp.m.oldp = 0

Bu 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.

go
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.

go
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.

go
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.

go
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.

go
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.

go
mp := getg().m
casgstatus(gp, _Grunning, _Gwaiting)
dropg()
schedule()

runtime.timesleep fonksiyonunda, t.f değerini belirten bu satır kod vardır:

go
t.f = goroutineReady

Bu 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.

go
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.

go
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:

go
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.

go
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:

go
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.

go
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.

go
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:

go
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.

go
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.

go
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:

  1. Yeni yığın alanı tahsis et
  2. Eski yığın belleğini doğrudan runtime.memmove aracılığıyla yeni yığın alanına kopyala
  3. Yığın işaretçileri içeren yapıları ayarla, örneğin defer, panic, vb.
  4. G'nin yığın alanı alanını güncelle
  5. Eski yığın belleğine işaret eden işaretçileri runtime.adjustframe aracılığıyla ayarla
  6. 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.

go
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.

go
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.

go
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:

go
// local runq
if gp, inheritTime := runqget(pp); gp != nil {
    return gp, inheritTime, false
}

Yerel kuyrukta G yoksa, sonra global kuyruktan almaya çalışın:

go
// 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:

go
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.

go
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.

go
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.

go
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:

  1. P'nin yerel kuyruğu
  2. Global kuyruk
  3. Ağ poller
  4. 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.

go
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:

go
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:

  1. M kilitli değil
  2. Şu anda bellek tahsis etmiyor
  3. Önleme devre dışı değil
  4. P _Prunning durumunda

Aşağıdaki iki durumda, g.stackguard0 stackPreempt olarak ayarlanır:

  • Çöp toplama gerektiğinde
  • Bir sistem çağrısı oluştuğunda
go
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.

go
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ı:

go
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.

go
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.

go
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.

go
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.

go
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.

go
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)
  ...
  RET

Mevcut 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.

go
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.

Golang by www.golangdev.cn edit