UN NewsUnited Kingdom announces action on disinformation and global AI standardsDaily MaverickGROOT BOEREWORS TREK: Succulent & spicy — the very best boerie in the landThe Jerusalem PostLikud rejects Otzma Yehudit's Ben-Gvir's push for Defense Ministry, Justice Ministry for GotlivPunchHow to change phone number, email on JAMB portalInquirerPalace open to amendments to anti-espionage law, says CastroCollider'Interview With the Vampire' Director Officially Reveals Why Louis and Lestat’s Relationship Didn't Need to Be More Explicit [Exclusive]The South African48-hour water shutdown planned across parts of DurbanSCMP ChinaChina hawks grumble over Trump’s lavish welcome planned for Xi state visitMalay MailAsian Games: Golden girl Adeola to take Proton e.Mas home to Sabah for familyVanguardEkiti poll: Appeal Court rejects request to relocate sitting as SDP counsel snubs tribunalSRF NewsKrieg in der Ukraine – Erneute Angriffe auf Kiew – Zwei ToteObservador DesportoPS questiona IP sobre estrada fechada desde a Kristin
The Daily Newsstand · Free, Always
Wednesday, September 23, 2026

Запустили 1 000 000 горутин на Go. Вот сколько это стоило

Translate

Каждый, кто касался Go, наверняка слышал о том, что горутина — это «легковесный поток», который управляется самим рантаймом, и именно поэтому вы можете запускать тысячи и более горутин. Нас точно не обманывают, но давайте разберёмся, почему это так и какой в этом компромисс.

Что именно будем измерять

Я планирую замерить время запуска программы, объём потребляемой оперативной памяти и загрузку процессора, а также проверить, сколько горутин действительно существует внутри программы. Будем двигаться постепенно от меньшего к большему, а то вдруг мой ноутбук-старичок споткнётся раньше.

Что я ожидаю

Я слышал, что горутина весит от 2 до 8 КБ в зависимости от ОС. Окей, возьмём по максимуму, и пусть миллион горутин займёт 8 ГБ оперативной памяти. Нагрузку на процессор, честно сказать, не представляю, как считать, поэтому накину на глаз: пусть будет 67 процентов. А время запуска программы прикинем так: пусть запуск займёт до 5 секунд.

Запускаем 1 000 000 горутин

Для эксперимента мне нужно, чтобы каждая горутина:

  1. реально стартовала;

  2. сообщила об этом;

  3. не завершилась до момента измерения.

Поэтому внутри каждой горутины полезной работы специально нет:

go func() {
    wg.Done()
    <-block
}()

wg.Done() показывает, что горутина уже начала выполняться, а чтение из block оставляет её жить и переводит в ожидание.

После запуска всех горутин вызывается:

wg.Wait()

Поэтому к моменту измерений каждая из них гарантированно успела хотя бы начать выполнение.

Полный код эксперимента:


package main  
  
import (  
    "flag"  
    "fmt"    
    "runtime"  
    "sync"  
    "time"
)  
  
func main() {  
    n := flag.Int("n", 1_000_000, "number of goroutines")  
    flag.Parse()  
  
    block := make(chan struct{})  
  
    var wg sync.WaitGroup  
    wg.Add(*n)  
  
    var before runtime.MemStats  
    runtime.ReadMemStats(&before)  
  
    start := time.Now()  
  
    for i := 0; i < *n; i++ {  
       go func() {  
          wg.Done()  
          <-block  
       }()  
    }  
  
    wg.Wait()  
  
    elapsed := time.Since(start)  
  
    var after runtime.MemStats  
    runtime.ReadMemStats(&after)  
  
    fmt.Printf("Goroutines:   %d\n", runtime.NumGoroutine())  
    fmt.Printf("Create time:  %s\n", elapsed)  
    fmt.Printf("HeapAlloc:    %.2f MB\n", float64(after.HeapAlloc)/1024/1024)  
    fmt.Printf("HeapSys:      %.2f MB\n", float64(after.HeapSys)/1024/1024)  
    fmt.Printf("StackInuse:   %.2f MB\n", float64(after.StackInuse)/1024/1024)  
    fmt.Printf("StackSys:     %.2f MB\n", float64(after.StackSys)/1024/1024)  
    fmt.Printf("Sys:          %.2f MB\n", float64(after.Sys)/1024/1024)  
  
    fmt.Printf(  
       "HeapAlloc delta: %.2f MB\n",  
       float64(after.HeapAlloc-before.HeapAlloc)/1024/1024,  
    )  
  
    fmt.Println("\nPress Enter to exit...")  
    fmt.Scanln()  
  
    close(block)  
}

Результаты

Goroutine

Время создания и старта

StackInuse

Sys

Working Set

Private Memory

1 000

6.0 ms

7.94 MiB

15.27 MiB

14.33 MiB

21.66 MiB

10 000

55.5 ms

78.38 MiB

92.08 MiB

90.99 MiB

98.16 MiB

100 000

628 ms

781.50 MiB

854.77 MiB

849.27 MiB

858.95 MiB

500 000

3.92 s

3906.53 MiB

4224.45 MiB

4217.69 MiB

4234.79 MiB

1 000 000

7.92 s

7812.81 MiB

8457.53 MiB

8107.91 MiB

8446.18 MiB

Миллион действительно запустился

При -n 1000000 программа показала:

Goroutines: 1000001

Почему миллион и ещё одна?

Потому что, помимо нашего миллиона, у программы есть основная горутина main.Моя оценка по памяти неожиданно оказалась очень близкой. А вот со временем уже нет:

7.92 секунды

вместо ожидаемых пяти.

Рост при этом получился довольно понятным:

1K       → 6 ms
10K      → 55.5 ms
100K     → 628 ms
500K     → 3.92 s
1M       → 7.92 s

На больших значениях время выглядит близким к линейному росту относительно количества горутин.

Теперь самое интересное. Посмотрим только на StackInuse:

1 000       →    7.94 MiB
10 000      →   78.38 MiB
100 000     →  781.50 MiB
500 000     → 3906.53 MiB
1 000 000   → 7812.81 MiB

Пересчитаем последний результат:

7812.81 MiB × 1024 / 1 000 000 ≈ 8 KiB

Практически ровно столько я и заложил в ожидания. Я случайно попал? Не совсем…

Лезем в runtime/stack.go.

Минимальный стек для Go-кода задаётся как:

stackMin = 2048

То есть 2 KiB.

Но для Windows рантайм добавляет дополнительное пространство:

stackSystem = goos.IsWindows*4096 + ...

Получаем:

2048 + 4096 = 6144 байт

После этого рантайм округляет размер выделяемого стека вверх до подходящей степени двойки:

8192 байта = 8 KiB

А что с CPU?

Вот здесь мой прогноз в 67% оказался совсем мимо. После того как миллион горутин успел стартовать, процессор практически вернулся к обычной загрузке. На первый взгляд странно. У меня ведь существует миллион горутин. Почему CPU не горит? Причина находится здесь:

<-block

Каждая горутина доходит до чтения из канала, данных в котором нет, и блокируется. Рантайм паркует такую горутину: она продолжает существовать, её стек занимает память, рантайм хранит её состояние, но выполнять на CPU ей сейчас нечего.

Получается важное различие:

существующая горутина != выполняющаяся горутина

Поэтому миллион ожидающих горутин может занимать гигабайты памяти и при этом практически не потреблять процессорное время после того, как все они были запущены и припаркованы.

Вывод

Итак, мой ноутбук действительно пережил 1 000 000 одновременно существующих горутин. Go не взорвался.

Но эксперимент хорошо показывает, что фраза:

горутины дешёвые

нуждается в продолжении.

Они позволяют строить конкурентный код значительно дешевле, чем модель «одна задача — один системный поток», но каждая горутина всё равно требует памяти и работы runtime.

View the original on Хабр

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.