GMP & Concurrency Model Deep Dive#


I. GMP Efficient Scheduling#

1. Core Concepts#

ComponentFull NameDescription
G (Goroutine)Lightweight threadStack starts at 2KB (dynamically growable), contains program counter, stack pointer, register state
M (Machine)OS threadCorresponds to OS thread, responsible for executing G’s code, contains g0 (scheduling stack), curg (current G), m0 (main thread)
P (Processor)Logical processorCount determined by GOMAXPROCS, holds local run queue (LRQ, capacity 256), responsible for scheduling decisions and context switching

G States: _Gidle, _Grunnable, _Grunning, _Gsyscall, _Gwaiting, _Gdead

2. Design Principles#

type g struct {
    stack        stack    // stack info
    sched        gobuf    // scheduling context
    atomicstatus uint32   // G state
    goid         int64    // G ID
    m            *m       // currently bound M
    // ...
}

type p struct {
    id       int32
    status   uint32      // _Pidle, _Prunning
    runqhead uint32      // local queue head
    runqtail uint32      // local queue tail
    runq     [256]guintptr  // local queue
    runnext  guintptr    // next G to run (highest priority)
    // ...
}

type m struct {
    g0       *g     // scheduling stack
    curg     *g     // current G
    p        *p     // currently bound P
    nextp    *p     // next P (for wake-up)
    spinning bool   // whether doing work stealing
    // ...
}

3. Scheduling Strategies#

3.1 Scheduling Loop (schedule function)#

func schedule() {
    _g_ := getg()

    // Check global queue every 61 scheduling cycles
    if _g_.m.p.ptr().schedtick%61 == 0 && sched.runqsize > 0 {
        // Get one G from global queue
        gp := globrunqget(_g_.m.p.ptr(), 1)
        if gp != nil {
            execute(gp)
        }
    }

    // Get G from local queue, global queue, or netpoller
    gp, inheritTime := runqget(_g_.m.p.ptr())
    if gp == nil {
        gp, inheritTime = findrunnable() // blocks until G found
    }

    execute(gp)
}

3.2 Four Main Scheduling Triggers#

TriggerConditionImplementation
Active schedulingCalling Gosched()Put current G back to queue, yield P
Passive schedulingBlocking operations (syscall, channel, lock)G state switches to _Gsyscall or _Gwaiting
Preemptive schedulingGo 1.14+, signal-basedSend SIGURG signal, stack growth detection
Syscall schedulingEntering syscallRelease P, M enters syscall, wake idle M

Active scheduling:

func Gosched() {
    // Put current G back to queue, yield P
    goparkunlock(&runtime.sched.lock, waitReasonGosched, traceEvGoSched, 1)
}

Preemptive scheduling:

// Signal-based preemptive scheduling
func preemptM(mp *m) {
    // Send SIGURG signal
    signalM(mp, sigPreempt)
}

// Stack growth detection (cooperative)
func morestack() {
    // Check if preemption is needed
    preempt = stackguard0 == stackPreempt
}

Syscall scheduling:

func entersyscall() {
    // Release P, M enters syscall
    pp := _g_.m.p.ptr()
    _g_.m.p = 0
    pp.status = _Psyscall

    // If there are idle P's and no spinning M, wake one
    if sched.npidle > 0 && sched.nmspinning == 0 {
        wakep()
    }
}

4. Work Stealing Mechanism#

func findrunnable() (gp *g, inheritTime bool) {
    pp := _g_.m.p.ptr()

    // 1. Local queue
    if gp, inheritTime := runqget(pp); gp != nil {
        return gp, inheritTime
    }

    // 2. Global queue
    if sched.runqsize != 0 {
        lock(&sched.lock)
        gp := globrunqget(pp, 0)
        unlock(&sched.lock)
        if gp != nil {
            return gp, false
        }
    }

    // 3. Netpoller
    if netpollinited() && atomic.Load(&netpollWaiters) > 0 {
        if list := netpoll(0); !list.empty() {
            gp := list.pop()
            injectglist(&list)
            return gp, false
        }
    }

    // 4. Work stealing - steal half from other P's
    for i := 0; i < 4; i++ {
        for enum := stealOrder.start(fastrand()); !enum.done(); enum.next() {
            if sched.gcwaiting != 0 {
                return nil, false
            }
            p2 := allp[enum.position()]
            if pp == p2 {
                continue
            }
            if gp := runqsteal(pp, p2, stealRunNextG); gp != nil {
                return gp, false
            }
        }
    }

    // 5. Idle M yields P
    if sched.runqsize != 0 {
        return nil, false
    }

    // 6. Block and wait
    stopm()
    return nil, false
}

5. Performance Optimization Keys#

5.1 P Count Selection#

// Best practice: P count = CPU cores
GOMAXPROCS = runtime.NumCPU()

// I/O intensive: can set more P's
// CPU intensive: equal to core count

5.2 Local Queue Optimization#

  • Prioritize runnext (LIFO, cache-friendly)
  • Local queue capacity 256, avoiding frequent lock contention
  • Batch transfer (stealWork)

5.3 Memory Allocation Optimization#

  • P holds mcache, avoiding cross-P contention
  • mcentral cache, mheap global heap

6. Deep Dive Analysis#

6.1 Syscall Optimization#

// ❌ Bad: syscall on every read
for {
    syscall.Read(fd, buf)
}

// ✅ Good: batch reading
bufio.NewReader(fd).Read(buf)

6.2 Lock Contention Issues#

// Use P-localization technique
type LocalCache struct {
    data []int
    mu   sync.Mutex
}

// Optimization: private cache per P
var localCaches [runtime.GOMAXPROCS(0)]*LocalCache

7. Performance Monitoring#

7.1 GODEBUG Scheduler Trace#

GODEBUG=schedtrace=1000,scheddetail=1 ./program
# Output example:
# SCHED 1004ms: gomaxprocs=8 idleprocs=4 threads=5 spinningthreads=0 idlethreads=0 runqueue=0 [0 0 0 0 0 0 0 0]

7.2 pprof Analysis#

import _ "net/http/pprof"

// View goroutine blocking
curl http://localhost:6060/debug/pprof/goroutine?debug=2

// Trace analysis
f, _ := os.Create("trace.out")
trace.Start(f)
defer trace.Stop()
// Analyze: go tool trace trace.out

8. Optimization Techniques#

8.1 Goroutine Pooling#

type Pool struct {
    work chan func()
    sem  chan struct{}
}

func (p *Pool) Schedule(task func()) {
    select {
    case p.work <- task:
    case p.sem <- struct{}{}:
        go p.worker(task)
    }
}

8.2 Lock-Free Queue Using P Locality#

// Leverage P's local queue mechanism
var taskQueues [runtime.GOMAXPROCS(0)]chan Task

func Submit(task Task) {
    pid := runtime_procPin()
    taskQueues[pid] <- task
    runtime_procUnpin()
}

8.3 NUMA-Aware Scheduling (Go 1.21+)#

import "runtime"

runtime.SetCPUAffinity([]int{0, 2, 4, 6}) // Bind to NUMA node 0

9. Scheduling Evolution#

VersionFeatureDescription
Go 1.0Basic GMPGoroutine-based scheduling
Go 1.1Performance boostM:N scheduler
Go 1.2Preemption attemptCooperative-based
Go 1.5Improved PWork stealing optimization
Go 1.8More aggressiveSyscall optimization
Go 1.14Asynchronous preemptionSignal-based preemption
Go 1.21NUMA-awareImproved multi-core performance

GMP Scheduling Core Advantages:

  • M:N model: Few threads can host a large number of goroutines
  • Work stealing: Dynamically balance load, maximize CPU utilization
  • P localization: Reduce lock contention, improve cache hit rate
  • Preemptive: Avoid long occupation causing starvation of other G’s
  • Adaptive: Dynamically adjust scheduling strategy based on load

Understanding GMP is crucial for writing efficient programs, especially when handling mixed scenarios of high concurrency, I/O intensive, and CPU intensive workloads, enabling targeted optimization of goroutine usage patterns.


II. Goroutine Channels & Concurrency Patterns#

1. Goroutine Deep Dive#

Essentially a user-space coroutine.

2. Channel Implementation#

2.1 Memory Structure#

type hchan struct {
    qcount   uint           // current queue element count
    dataqsiz uint           // circular queue size
    buf      unsafe.Pointer // pointer to ring buffer
    elemsize uint16
    closed   uint32
    elemtype *_type
    sendx    uint   // send index
    recvx    uint   // receive index
    recvq    waitq  // waiting receiver G queue
    sendq    waitq  // waiting sender G queue
    lock     mutex  // mutex lock
}

2.2 Unbuffered vs Buffered#

// Unbuffered: synchronous communication, handoff mode
unbuf := make(chan int)

// Underlying flow:
// Sender G1 -> chan lock -> finds G2 in recvq -> directly copies data to G2 -> wakes G2
// Implements lock-free exchange, no intermediate buffer

// Buffered: asynchronous communication, circular queue
buf := make(chan int, 10)

// Underlying flow:
// Send: lock -> put into buf[sendx] -> sendx++ -> unlock
// Receive: lock -> take from buf[recvx] -> recvx++ -> unlock
// Doesn't block unless full/empty

3. Concurrency Patterns#

3.1 Generator Pattern#

func generator(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for _, n := range nums {
            out <- n
        }
    }()
    return out
}

func main() {
    ch := generator(1, 2, 3, 4)
    for v := range ch {
        println(v)
    }
}

3.2 Pipeline Pattern#

// Pipeline processing
func multiply(done <-chan struct{}, in <-chan int, factor int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for n := range in {
            select {
            case out <- n * factor:
            case <-done:
                return
            }
        }
    }()
    return out
}

// Usage: 1 -> x2 -> +1
func main() {
    done := make(chan struct{})
    defer close(done)

    gen := generator(1, 2, 3)
    mult := multiply(done, gen, 2)
    add := add(done, mult, 1)

    for v := range add {
        println(v) // 3, 5, 7
    }
}

3.3 Fan-in / Fan-out#

// Fan-out: distribute tasks to multiple workers
func fanOut(in <-chan int, workers int) []<-chan int {
    channels := make([]<-chan int, workers)
    for i := 0; i < workers; i++ {
        channels[i] = worker(in)
    }
    return channels
}

// Fan-in: merge multiple channels
func fanIn(channels ...<-chan int) <-chan int {
    var wg sync.WaitGroup
    out := make(chan int)

    multiplex := func(c <-chan int) {
        defer wg.Done()
        for n := range c {
            out <- n
        }
    }

    wg.Add(len(channels))
    for _, c := range channels {
        go multiplex(c)
    }

    go func() {
        wg.Wait()
        close(out)
    }()

    return out
}

3.4 Select Multiplexing#

// Non-blocking operation
func tryReceive(ch chan int) (int, bool) {
    select {
    case v := <-ch:
        return v, true
    default:
        return 0, false
    }
}

// Timeout control
func withTimeout(ch chan int, timeout time.Duration) (int, error) {
    select {
    case v := <-ch:
        return v, nil
    case <-time.After(timeout):
        return 0, errors.New("timeout")
    }
}

// Heartbeat detection
func heartbeat(interval time.Duration) <-chan struct{} {
    heart := make(chan struct{})
    go func() {
        ticker := time.NewTicker(interval)
        defer ticker.Stop()
        for {
            select {
            case <-ticker.C:
                heart <- struct{}{}
            }
        }
    }()
    return heart
}

3.5 Worker Pool#

type Task func() interface{}

type Pool struct {
    tasks   chan Task
    results chan interface{}
    workers int
    wg      sync.WaitGroup
    quit    chan struct{}
}

func NewPool(workers, buffer int) *Pool {
    return &Pool{
        tasks:   make(chan Task, buffer),
        results: make(chan interface{}, buffer),
        workers: workers,
        quit:    make(chan struct{}),
    }
}

func (p *Pool) worker(id int) {
    defer p.wg.Done()
    for {
        select {
        case task, ok := <-p.tasks:
            if !ok {
                return
            }
            p.results <- task()
        case <-p.quit:
            return
        }
    }
}

func (p *Pool) Start() {
    p.wg.Add(p.workers)
    for i := 0; i < p.workers; i++ {
        go p.worker(i)
    }
}

func (p *Pool) Submit(task Task) {
    p.tasks <- task
}

func (p *Pool) Results() <-chan interface{} {
    return p.results
}

func (p *Pool) Stop() {
    close(p.quit)
    p.wg.Wait()
    close(p.results)
}

3.6 Tee Channel (Branching)#

// Duplicate a single channel to multiple outputs
func tee(in <-chan int, count int) []<-chan int {
    outs := make([]chan int, count)
    for i := range outs {
        outs[i] = make(chan int)
    }

    go func() {
        defer func() {
            for _, out := range outs {
                close(out)
            }
        }()

        for v := range in {
            for _, out := range outs {
                out <- v
            }
        }
    }()

    result := make([]<-chan int, count)
    for i, out := range outs {
        result[i] = out
    }
    return result
}

3.7 Context Propagation and Cancellation#

// Hierarchical cancellation
func worker(ctx context.Context, id int, in <-chan int) {
    for {
        select {
        case v, ok := <-in:
            if !ok {
                return
            }
            fmt.Printf("Worker %d: %d\n", id, v)
        case <-ctx.Done():
            fmt.Printf("Worker %d cancelled: %v\n", id, ctx.Err())
            return
        }
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    ch := make(chan int)

    for i := 0; i < 3; i++ {
        go worker(ctx, i, ch)
    }

    // Send data...
    time.Sleep(time.Second)
    cancel() // Cancel all workers
}

4. Advanced Concurrency Techniques#

4.1 Lock-Free Channel#

// Single Producer Single Consumer (SPSC) using ring buffer lock-free
type SPSCQueue struct {
    buffer []interface{}
    size   uint64
    mask   uint64
    head   uint64 // consumer index
    tail   uint64 // producer index
}

func NewSPSCQueue(size uint64) *SPSCQueue {
    // size must be power of 2
    return &SPSCQueue{
        buffer: make([]interface{}, size),
        size:   size,
        mask:   size - 1,
    }
}

// Lock-free production
func (q *SPSCQueue) TryPush(v interface{}) bool {
    head := atomic.LoadUint64(&q.head)
    tail := atomic.LoadUint64(&q.tail)

    if tail+1 == head || (tail == q.size-1 && head == 0) {
        return false // full
    }

    q.buffer[tail&q.mask] = v
    atomic.StoreUint64(&q.tail, tail+1)
    return true
}

4.2 Nil Channel#

// nil channel always blocks in select
func merge(chs ...<-chan int) <-chan int {
    out := make(chan int)
    for _, ch := range chs {
        go func(c <-chan int) {
            for v := range c {
                out <- v
            }
        }(ch)
    }
    return out
}

// Improved: dynamically remove closed channels
func mergeDynamic(chs ...<-chan int) <-chan int {
    out := make(chan int)
    var wg sync.WaitGroup

    for i, ch := range chs {
        wg.Add(1)
        go func(idx int, c <-chan int) {
            defer wg.Done()
            for v := range c {
                out <- v
            }
            chs[idx] = nil // mark as nil
        }(i, ch)
    }

    go func() {
        wg.Wait()
        close(out)
    }()
    return out
}

5. Performance Optimization#

5.1 Channel Capacity#

// Throughput vs Latency
// Unbuffered: lowest latency, lowest throughput
// Buffered: higher throughput, increased latency jitter

// Heuristic formula
optimalBufferSize = (1 + expectedBurstSize) * numberWorkers

// Practical benchmarking
func benchmarkBuffer(size int) {
    ch := make(chan int, size)
    // When producer and consumer speeds mismatch, buffer smooths fluctuations
}

5.2 Common Pitfalls#

// ❌ 1. Producer not closing channel causes goroutine leak
func leak() {
    ch := make(chan int)
    go func() {
        for v := range ch { // never exits
            fmt.Println(v)
        }
    }()
    ch <- 1
    // ch not closed, goroutine blocks forever
}

// ❌ 2. Sending to closed channel panics
func bad() {
    ch := make(chan int)
    close(ch)
    ch <- 1 // panic
}

// ✅ 3. Reading from closed channel returns zero value immediately
func ok() {
    ch := make(chan int)
    close(ch)
    v := <-ch      // v = 0, no panic
    v, ok := <-ch  // ok = false
}

// Detecting goroutine leaks
import "runtime/pprof"

func detectLeak() {
    before := runtime.NumGoroutine()
    // Execute operation...
    time.Sleep(time.Second)
    after := runtime.NumGoroutine()

    if after > before {
        fmt.Printf("Leak detected: %d -> %d\n", before, after)
        // Print stack to find leak point
        pprof.Lookup("goroutine").WriteTo(os.Stdout, 1)
    }
}

// Deadlock detection (compile time)
// go build -race detects data race
// Use go-deadlock package to detect deadlock
import "github.com/sasha-s/go-deadlock"

var mu deadlock.Mutex // Replace sync.Mutex

6. Architecture Design Principles#

  1. Don’t communicate by sharing memory; share memory by communicating
  2. Pipeline depth 3-5 levels, deeper increases latency
  3. Worker Pool size = runtime.GOMAXPROCS(0) * 2-4 (higher for I/O intensive)
  4. Channel lifecycle is clear: whoever creates, closes
  5. Use context for timeout and cancellation, avoid infinite blocking
  6. Prefer buffered channel + select default for non-blocking
  7. Benchmark to validate concurrency models, don’t prematurely optimize
你又来啦!好开心 🐟