Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to overcome goroutine overhead?

Tags:

go

goroutine

I quite new to go. I need some help.

I guess I am facing a goroutine overhead. I was wondering if I can overcome the go-routine overhead. Here is my code.

package main

import (
    "log"
    "sync"
    "time"
)

// This function will be replaced by receiving websocket messages.
func msg(i int) int {
    return i
}

// This function will be replaced by an action, in response of the messeage received.
func action(i int, wg *sync.WaitGroup) {
    defer wg.Done() // decrease the counter by 1 when the task is done.
    log.Println(i, "th action start")
    time.Sleep(500 * time.Millisecond)
    log.Println(i, "th action end")
}

func main() {
    log.SetFlags(log.LstdFlags | log.Lmicroseconds)
    var wg sync.WaitGroup

    // I want to remove this part, without facing the overhead.
    // wg.Add(1)
    // go action(-1, &wg)
    // time.Sleep(1 * time.Second)
    // I want to remove this part, without facing the overhead.

    for i := 0; i < 10; i++ {
        time.Sleep(100 * time.Millisecond)
        msg := msg(i)
        log.Println(msg, "th message received")
        wg.Add(1) // increment the counter by 1
        go action(msg, &wg)
    }
    wg.Wait()
}

You can see the result(namely, Result1) below. There is 0.05s gap between the 0th message received and 0th action start. So, I guess I am facing a go-routine overhead. Once I unblock the code

    wg.Add(1)
    go action(-1, &wg)
    time.Sleep(1 * time.Second)

, you can see the result(namely, Result2) below. There is only 0.0003s gap between the 0th message received and 0th action start. But, I would like to remove this part, because it calls action unnecessarily. Any suggestions would be appreciated.

Result 1

2025/08/31 17:12:15.915057 0 th message received
2025/08/31 17:12:15.968358 0 th action start
2025/08/31 17:12:16.069100 1 th message received
2025/08/31 17:12:16.069100 1 th action start
2025/08/31 17:12:16.169469 2 th message received
2025/08/31 17:12:16.169469 2 th action start
2025/08/31 17:12:16.270079 3 th message received
2025/08/31 17:12:16.270079 3 th action start
...

Result 2

2025/08/31 17:15:36.098222 -1 th action start
2025/08/31 17:15:36.653260 -1 th action end
2025/08/31 17:15:37.199560 0 th message received
2025/08/31 17:15:37.199866 0 th action start
2025/08/31 17:15:37.300656 1 th message received
2025/08/31 17:15:37.300656 1 th action start
2025/08/31 17:15:37.401045 2 th message received
2025/08/31 17:15:37.401562 2 th action start
2025/08/31 17:15:37.501994 3 th message received
2025/08/31 17:15:37.502418 3 th action start
...
like image 876
Sunghoon Lim Avatar asked Oct 16 '25 16:10

Sunghoon Lim


1 Answers

That ~50 ms gap before the first action is not real goroutine overhead; it is a cold start of the Go runtime.

The first go action(...) has to spin up an OS thread, initialize the scheduler, bring up timers and the netpoller, touch memory pages, etc.

Once that initialization is complete, new goroutines start almost instantly, so only the very first one shows the delay. Your go action(-1, …) just forces this warm-up earlier so the real work appears to start without lag.


To avoid that 50 ms delay without calling a fake action, you can keep a pool of worker goroutines alive that wait on a channel for work so the first message is picked up immediately. From your code, something like this:

package main

import (
    "log"
    "runtime"
    "sync"
    "time"
)

func action(i int) {
    log.Println(i, "th action start")
    time.Sleep(500 * time.Millisecond)
    log.Println(i, "th action end")
}

func main() {
    log.SetFlags(log.LstdFlags | log.Lmicroseconds)

    workers := runtime.NumCPU() // or whatever
    tasks := make(chan int)
    var wg sync.WaitGroup

    // Start workers so they stay hot and wait for tasks
    for w := 0; w < workers; w++ {
        go func() {
            for i := range tasks {
                action(i)
                wg.Done()
            }
        }()
    }

    for i := 0; i < 10; i++ {
        time.Sleep(100 * time.Millisecond)
        log.Println(i, "th message received")
        wg.Add(1)
        tasks <- i
    }

    
    close(tasks) // workers exit
    wg.Wait()
}
like image 147
aran Avatar answered Oct 18 '25 06:10

aran