SoFunction
Updated on 2025-04-11

A brief analysis of the key mechanism and performance of scheduler in Golang

Golang's scheduler is the core component of its concurrency model, responsible for managing the scheduling and execution of Goroutines. Goroutine is a lightweight thread in the Go language and is managed by the Go runtime.

The scheduler is designed to efficiently utilize multi-core CPUs while maintaining low latency and high throughput. Below we analyze the key mechanism of Golang scheduler from the theoretical and code level.

1. Basic concepts of schedulers

1.1 Goroutine

Goroutine is a concurrent execution unit in the Go language, which is lighter than operating system threads. Each Goroutine initially only takes up a few KB of stack space, and the stack space can grow or shrink dynamically as needed.

1.2 M-P-G Model

The Go scheduler adopts the M-P-G model:

  • M (Machine): Represents the operating system thread (OS Thread), scheduled by the operating system.
  • P (Processor): represents the logical processor, responsible for scheduling Goroutine. The number of P is usually equal to the number of CPU cores and can be set through GOMAXPROCS.
  • G (Goroutine): represents the concurrent execution unit of Go.

2. The core mechanism of the scheduler

2.1 Work Stealing

When there is no running Goroutine in the local queue of one P, it tries to "steal" Goroutine from the queue of other P to execute. This mechanism can balance the load of each P, avoiding some Ps being idle and others being overloaded.

2.2 Preemptive scheduling

The Go scheduler is preemptive, meaning it can force switching execution rights during Goroutine execution. Go 1.14 introduces a signal-based preemption mechanism to ensure that long-running Goroutines do not block the execution of other Goroutines.

2.3 System calls

When Goroutine executes a system call, the scheduler separates the current M from P and creates a new M to execute the system call. This prevents the system call from blocking the execution of the entire P.

3. Code parsing

3.1 Initialization of the scheduler

The initialization of the scheduler is completed in the schedinit function in runtime/. This function sets the number of P, initializes the global queue, etc.

func schedinit() {
    // The number of initialized P    procs := int(ncpu)
    if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
        procs = n
    }
    if procresize(procs) != nil {
        throw("unknown runnable goroutine during bootstrap")
    }
}

3.2 Goroutine creation and scheduling

The creation of Goroutine is triggered by the go keyword, and the function will eventually be called. This function will put the new Goroutine into the local queue of the current P.

func newproc(siz int32, fn *funcval) {
    argp := add((&fn), )
    gp := getg()
    pc := getcallerpc()
    systemstack(func() {
        newg := newproc1(fn, argp, siz, gp, pc)
        _p_ := getg().()
        runqput(_p_, newg, true)
        if mainStarted {
            wakep()
        }
    })
}

3.3 Scheduling loop

The core of the scheduler is the schedule function located in runtime/. This function will obtain a runnable Goroutine from the local queue, global queue or other queues of P and execute it.

func schedule() {
    _g_ := getg()

    if _g_. != 0 {
        throw("schedule: holding locks")
    }

    if _g_. != 0 {
        stoplockedm()
        execute(_g_.(), false) // Never returns.
    }

    // Scheduling loop    for {
        if  != 0 {
            gcstopm()
            continue
        }
        if _g_.().runqempty() {
            // If the local queue is empty, try to steal the Goroutine from the global queue or other P            gp, inheritTime = runqget(_g_.())
            if gp == nil {
                gp, inheritTime = findrunnable() // Block until a runnable Goroutine is found            }
        } else {
            // Get Goroutine from local queue            gp, inheritTime = runqget(_g_.())
        }
        execute(gp, inheritTime)
    }
}

3.4 Preemption mechanism

Go 1.14 introduces a signal-based preemption mechanism to ensure that long-running Goroutines do not block the execution of other Goroutines. The implementation of the preemption mechanism is located in runtime/signal_unix.go.

func preemptM(mp *m) {
    if (&, 0, 1) {
        signalM(mp, sigPreempt)
    }
}

4. Performance and concurrency

4.1 Efficient use of multi-core

Through the M-P-G model, the Go scheduler can efficiently utilize multi-core CPUs. Each P is bound to an M, which is the operating system thread, and P is responsible for scheduling the Goroutine. The number of P is usually equal to the number of CPU cores, which maximizes the use of CPU resources.

4.2 Low latency

The preemptive scheduling and signal-based preemption mechanism of the Go scheduler ensure low latency. Even if a Goroutine is running for a long time, the scheduler can switch execution rights in time to prevent other Goroutines from being blocked for a long time.

4.3 High throughput

The work theft mechanism ensures load balancing between each P, avoiding the situation where some P is overloaded while others are idle. This mechanism improves the overall throughput of the system.

5. Summary

Golang's scheduler achieves efficient concurrent and parallel execution through mechanisms such as M-P-G model, work theft, preemptive scheduling, etc. The scheduler is designed to make Go excellent in handling high concurrency scenarios, making full use of multi-core CPU resources while maintaining low latency and high throughput.

By deeply understanding the working principles of schedulers, developers can better write efficient concurrent programs and make full use of the concurrency characteristics of Go language.

This is the article about a brief analysis of the key mechanisms and performance of schedulers in Golang. For more related Golang schedulers, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!