Fundamentals 33 min read

Understanding Go's Concurrency Model: Goroutine, Scheduler (GMP) and Channels

Go implements a two‑level N:M concurrency model where lightweight Goroutines (G) run on logical processors (P) backed by OS threads (M), with the GMP scheduler managing run queues and channels—mutex‑protected circular buffers with send/receive queues—providing efficient, preemptive multitasking and communication.

Tencent Cloud Developer
Tencent Cloud Developer
Tencent Cloud Developer
Understanding Go's Concurrency Model: Goroutine, Scheduler (GMP) and Channels

Concurrent programming is a core concern for developers, and Go (Golang) is renowned for its built‑in high‑concurrency capabilities. This article explains the implementation of Goroutine and channel, the underlying thread models, and the Go scheduler.

1. Thread implementation models

There are three thread models:

User‑level thread model (N:1) : Managed entirely in user space; all user threads map to a single kernel scheduling entity (KSE). Lightweight but a blocking I/O operation blocks the whole process.

Kernel‑level thread model (1:1) : Each user thread is a kernel thread; true parallelism but higher overhead.

Two‑level thread model (N:M) : Combines the advantages of the above two models. User threads are scheduled onto multiple kernel threads (KSEs) by a user‑level scheduler.

Go adopts the two‑level model.

2. Go's concurrency mechanism

In Go, an independent control flow that is not managed by the OS kernel is called a Goroutine . Goroutine is a lightweight coroutine that, together with the Go scheduler, implements a two‑level thread model.

The scheduler uses three structures, known as the GMP model :

G : Represents a Goroutine. It stores the stack, status, and registers needed for context switching.

M : Represents an OS thread (kernel thread). Each M is bound to a single KSE.

P : Represents a logical processor. A P holds a run queue of ready Goroutines and provides execution context for Ms.

Key relationships:

M ↔ KSE is 1:1.

P ↔ M is 1:1 (but a P can be reassigned to another M).

G ↔ P is many‑to‑one (a G must be attached to a P to run).

G structure (partial source)

<span>type g struct {</span>
<span>    stack      stack    // Goroutine stack range [stack.lo, stack.hi)</span>
<span>    stackguard0 uintptr // Used for preemptive scheduling</span>
<span>    m        *m      // Thread that runs this G</span>
<span>    sched      gobuf    // Scheduler data for this G</span>
<span>    atomicstatus uint32 // Goroutine state</span>
<span>    ...</span>
<span>}</span>

M structure (partial source)

<span>type m struct {</span>
<span>    g0      *g      // Special G for runtime tasks</span>
<span>    gsignal *g      // G for signal handling</span>
<span>    curg    *g      // Currently running G</span>
<span>    p      puintptr // Associated P</span>
<span>    nextp   puintptr // Potential P to bind</span>
<span>    oldp    puintptr // P before a system call</span>
<span>    spinning bool   // Whether M is searching for a runnable G</span>
<span>    lockedg *g      // G locked to this M</span>
<span>}</span>

P structure (partial source)

<span>type p struct {</span>
<span>    status   uint32</span>
<span>    m        muintptr // Associated M</span>
<span>    runqhead uint32</span>
<span>    runqtail uint32</span>
<span>    runq     [256]guintptr // Local run queue</span>
<span>    runnext  guintptr // Cached runnable G</span>
<span>    gFree    struct { gList int32 }</span>
<span>    ...</span>
<span>}</span>

3. Scheduler

The scheduler repeatedly calls runtime.schedule, which selects a runnable G from the local P run‑queue or the global run‑queue, or blocks waiting for work.

<span>func schedule() {</span>
<span>    _g_ := getg()</span>
<span>    // Try global run queue every 61 calls</span>
<span>    if gp == nil {</span>
<span>        if _g_.m.p.ptr().schedtick%61 == 0 && sched.runqsize > 0 {</span>
<span>            lock(&sched.lock)</span>
<span>            gp = globrunqget(_g_.m.p.ptr(), 1)</span>
<span>            unlock(&sched.lock)</span>
<span>        }</span>
<span>    }</span>
<span>    // Try local P run queue</span>
<span>    if gp == nil {</span>
<span>        gp, inheritTime = runqget(_g_.m.p.ptr())</span>
<span>    }</span>
<span>    // If still nil, block until a G becomes runnable</span>
<span>    if gp == nil {</span>
<span>        gp, inheritTime = findrunnable()</span>
<span>    }</span>
<span>    execute(gp, inheritTime)</span>
<span>}</span>

The execute function binds the selected G to the current M, changes its state to _Grunning, and finally jumps to the Goroutine’s entry point via runtime.gogo:

<span>func execute(gp *g, inheritTime bool) {</span>
<span>    _g_ := getg()</span>
<span>    _g_.m.curg = gp</span>
<span>    gp.m = _g_.m</span>
<span>    casgstatus(gp, _Grunnable, _Grunning)</span>
<span>    gp.stackguard0 = gp.stack.lo + _StackGuard</span>
<span>    if !inheritTime {</span>
<span>        _g_.m.p.ptr().schedtick++</span>
<span>    }</span>
<span>    gogo(&gp.sched)</span>
<span>}</span>

When a Goroutine finishes, runtime.goexit0 marks it as _Gdead, clears its fields, puts it back into the free list, and triggers a new scheduling cycle.

<span>func goexit0(gp *g) {</span>
<span>    casgstatus(gp, _Grunning, _Gdead)</span>
<span>    gp.m = nil</span>
<span>    // ... clear other fields ...</span>
<span>    gfput(_g_.m.p.ptr(), gp) // return to free list</span>
<span>    schedule()               // start next cycle</span>
<span>}</span>

4. Channels

Channels are the primary communication primitive between Goroutines. Internally they are represented by runtime.hchan, which includes a mutex‑protected circular buffer and two wait queues (send and receive).

<span>type hchan struct {</span>
<span>    qcount   uint   // Number of elements in the buffer</span>
<span>    dataqsiz uint   // Buffer size</span>
<span>    buf      unsafe.Pointer // Pointer to the buffer</span>
<span>    elemsize uint16 // Size of each element</span>
<span>    closed   uint32 // Closed flag</span>
<span>    elemtype *_type // Element type</span>
<span>    sendx    uint   // Send index</span>
<span>    recvx    uint   // Receive index</span>
<span>    recvq    waitq  // Waiting receivers</span>
<span>    sendq    waitq  // Waiting senders</span>
<span>    lock     mutex  // Protects the channel</span>
<span>}</span>

Creating a channel calls runtime.makechan, which allocates the hchan structure and, depending on element type and buffer size, allocates the buffer either together with the struct or separately.

<span>func makechan(t *chantype, size int) *hchan {</span>
<span>    elem := t.elem</span>
<span>    mem, overflow := math.MulUintptr(elem.size, uintptr(size))</span>
<span>    // ... error checks ...</span>
<span>    var c *hchan</span>
<span>    switch {</span>
<span>    case mem == 0:</span>
<span>        c = (*hchan)(mallocgc(hchanSize, nil, true))</span>
<span>        c.buf = c.raceaddr()</span>
<span>    case elem.ptrdata == 0:</span>
<span>        c = (*hchan)(mallocgc(hchanSize+mem, nil, true))</span>
<span>        c.buf = add(unsafe.Pointer(c), hchanSize)</span>
<span>    default:</span>
<span>        c = new(hchan)</span>
<span>        c.buf = mallocgc(mem, elem, true)</span>
<span>    }</span>
<span>    c.elemsize = uint16(elem.size)</span>
<span>    c.elemtype = elem</span>
<span>    c.dataqsiz = uint(size)</span>
<span>    return c</span>
<span>}</span>

Sending to a channel invokes runtime.chansend. The function first locks the channel, checks for closure, then follows three possible paths:

If there are waiting receivers, the value is copied directly to the receiver (bypassing the buffer).

If the buffer has free slots, the value is stored in the circular buffer.

Otherwise the sender is placed on the channel’s send queue and the Goroutine blocks via gopark.

<span>func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {</span>
<span>    if c == nil {</span>
<span>        if !block { return false }</span>
<span>        gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)</span>
<span>        throw("unreachable")</span>
<span>    }</span>
<span>    lock(&c.lock)</span>
<span>    if c.closed != 0 { unlock(&c.lock); panic("send on closed channel") }</span>
<span>    // Direct send to waiting receiver</span>
<span>    if sg := c.recvq.dequeue(); sg != nil {</span>
<span>        send(c, sg, ep, func(){ unlock(&c.lock) }, 3)</span>
<span>        return true</span>
<span>    }</span>
<span>    // Buffer has space?</span>
<span>    if c.qcount < c.dataqsiz {</span>
<span>        qp := chanbuf(c, c.sendx)</span>
<span>        typedmemmove(c.elemtype, qp, ep)</span>
<span>        c.sendx = (c.sendx + 1) % c.dataqsiz</span>
<span>        c.qcount++</span>
<span>        unlock(&c.lock)</span>
<span>        return true</span>
<span>    }</span>
<span>    // Blocked send</span>
<span>    if !block { unlock(&c.lock); return false }</span>
<span>    // enqueue sudog and park</span>
<span>    // ... (omitted for brevity) ...</span>
<span>    gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)</span>
<span>    // ...</span>
<span>}</span>

Receiving from a channel uses runtime.chanrecv with symmetric logic: it first checks for waiting senders, then for buffered data, and finally blocks the receiver if necessary.

<span>func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {</span>
<span>    if c == nil {</span>
<span>        if !block { return false, false }</span>
<span>        gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)</span>
<span>        throw("unreachable")</span>
<span>    }</span>
<span>    lock(&c.lock)</span>
<span>    if c.closed != 0 && c.qcount == 0 {</span>
<span>        unlock(&c.lock)</span>
<span>        if ep != nil { typedmemclr(c.elemtype, ep) }</span>
<span>        return true, false</span>
<span>    }</span>
<span>    // Direct receive from waiting sender</span>
<span>    if sg := c.sendq.dequeue(); sg != nil {</span>
<span>        recv(c, sg, ep, func(){ unlock(&c.lock) }, 3)</span>
<span>        return true, true</span>
<span>    }</span>
<span>    // Buffered data available?</span>
<span>    if c.qcount > 0 {</span>
<span>        qp := chanbuf(c, c.recvx)</span>
<span>        if ep != nil { typedmemmove(c.elemtype, ep, qp) }</span>
<span>        typedmemclr(c.elemtype, qp)</span>
<span>        c.recvx = (c.recvx + 1) % c.dataqsiz</span>
<span>        c.qcount--</span>
<span>        unlock(&c.lock)</span>
<span>        return true, true</span>
<span>    }</span>
<span>    // Blocked receive</span>
<span>    if !block { unlock(&c.lock); return false, false }</span>
<span>    // enqueue sudog and park</span>
<span>    // ... (omitted for brevity) ...</span>
<span>    gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)</span>
<span>    // ...</span>
<span>}</span>

Closing a channel calls runtime.closechan, which marks the channel as closed, wakes all waiting senders (which will panic) and receivers (which will return zero values), and clears pending sudog elements.

<span>func closechan(c *hchan) {</span>
<span>    lock(&c.lock)</span>
<span>    c.closed = 1</span>
<span>    var glist gList</span>
<span>    // Wake all receivers</span>
<span>    for { sg := c.recvq.dequeue(); if sg == nil { break }</span>
<span>        if sg.elem != nil { typedmemclr(c.elemtype, sg.elem); sg.elem = nil }</span>
<span>        gp := sg.g; gp.param = nil; glist.push(gp) }</span>
<span>    // Wake all senders (they will panic)</span>
<span>    for { sg := c.sendq.dequeue(); if sg == nil { break }</span>
<span>        gp := sg.g; gp.param = nil; glist.push(gp) }</span>
<span>    unlock(&c.lock)</span>
<span>    for !glist.empty() { gp := glist.pop(); gp.schedlink = 0; goready(gp, 3) }</span>
<span>}</span>

Overall, the combination of Goroutine, channel, and the GMP scheduler forms a robust and efficient concurrency model that underpins Go’s high‑performance server‑side development.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

GoSchedulerRuntimeGMPChannel
Tencent Cloud Developer
Written by

Tencent Cloud Developer

Official Tencent Cloud community account that brings together developers, shares practical tech insights, and fosters an influential tech exchange community.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.