Максимальное количество горутин

Сколько goroutines можно использовать безболезненно? Например, википедия говорит, что в Erlang может быть создано 20 миллионов процессов без снижения производительности.

Обновление: я только что немного изучил производительность goroutines и получил такие результаты:

  • Похоже, что время жизни goroutine больше вычисляет sqrt() 1000 раз (~ 45 мкс для меня), единственным ограничением является память
  • Горутин стоит 4 - 4,5 КБ

Ответ 1

Если goroutine заблокирован, нет никаких затрат, кроме:

  • Использование памяти
  • более медленная сборка мусора

Затраты (с точки зрения памяти и среднего времени для фактического запуска goroutine):

Go 1.6.2 (April 2016)
  32-bit x86 CPU (A10-7850K 4GHz)
    | Number of goroutines: 100000
    | Per goroutine:
    |   Memory: 4536.84 bytes
    |   Time:   1.634248 µs
  64-bit x86 CPU (A10-7850K 4GHz)
    | Number of goroutines: 100000
    | Per goroutine:
    |   Memory: 4707.92 bytes
    |   Time:   1.842097 µs

Go release.r60.3 (December 2011)
  32-bit x86 CPU (1.6 GHz)
    | Number of goroutines: 100000
    | Per goroutine:
    |   Memory: 4243.45 bytes
    |   Time:   5.815950 µs

На компьютере с установленной 4 ГБ памяти это ограничивает максимальное количество горутов чуть менее 1 миллиона.


Исходный код (нет необходимости читать это, если вы уже понимаете цифры, напечатанные выше):

package main

import (
    "flag"
    "fmt"
    "os"
    "runtime"
    "time"
)

var n = flag.Int("n", 1e5, "Number of goroutines to create")

var ch = make(chan byte)
var counter = 0

func f() {
    counter++
    <-ch // Block this goroutine
}

func main() {
    flag.Parse()
    if *n <= 0 {
            fmt.Fprintf(os.Stderr, "invalid number of goroutines")
            os.Exit(1)
    }

    // Limit the number of spare OS threads to just 1
    runtime.GOMAXPROCS(1)

    // Make a copy of MemStats
    var m0 runtime.MemStats
    runtime.ReadMemStats(&m0)

    t0 := time.Now().UnixNano()
    for i := 0; i < *n; i++ {
            go f()
    }
    runtime.Gosched()
    t1 := time.Now().UnixNano()
    runtime.GC()

    // Make a copy of MemStats
    var m1 runtime.MemStats
    runtime.ReadMemStats(&m1)

    if counter != *n {
            fmt.Fprintf(os.Stderr, "failed to begin execution of all goroutines")
            os.Exit(1)
    }

    fmt.Printf("Number of goroutines: %d\n", *n)
    fmt.Printf("Per goroutine:\n")
    fmt.Printf("  Memory: %.2f bytes\n", float64(m1.Sys-m0.Sys)/float64(*n))
    fmt.Printf("  Time:   %f µs\n", float64(t1-t0)/float64(*n)/1e3)
}

Ответ 2

Сотни тысяч, для каждого FAQ: Почему goroutines вместо потоков?:

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

Тест test/chan/goroutines.go создает 10 000 и может легко сделать больше, но предназначен для быстрого запуска; вы можете изменить число в вашей системе, чтобы экспериментировать. Вы можете легко запускать миллионы, учитывая достаточную память, например, на сервере.

Чтобы понять максимальное количество goroutines, обратите внимание, что стоимость per-goroutine - это прежде всего стек. За часто задаваемые вопросы:

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

Расчет на основе обратной связи заключается в том, чтобы предположить, что каждый горутин имеет один 4 KiB page, выделенный для стека (4 KiB - довольно однородный размер), плюс некоторые небольшие накладные расходы для блока управления (например, Блок управления потоками) для среды выполнения; это согласуется с тем, что вы наблюдали (в 2011 году Pre-Go 1.0). Таким образом, процедуры 100 Ki будут занимать около 400 Мбайт памяти, а 1 подпрограмма Mi займет около 4 ГБ памяти, которая по-прежнему управляется на рабочем столе, немного для телефона и очень управляема на сервере. На практике стартовый стек имеет размер от половины страницы (2 KiB) до двух страниц (8 KiB), поэтому это примерно правильно.

Размер начального стека со временем изменился; он начался с 4 KiB (одна страница), затем в 1.2 был увеличен до 8 KiB (2 страницы), затем в 1.4 был уменьшен до 2 KiB (половина страницы). Эти изменения произошли из-за сегментированных стеков, вызывающих проблемы с производительностью при быстром переключении между сегментами ( "расщепление горячих стеков" ), поэтому увеличено до уменьшения (1.2), а затем уменьшилось, когда сегментированные стеки были заменены смежными стеками (1.4):

Перейти 1.2 Примечания к выпуску: Размер стека:

В Go 1.2 минимальный размер стека при создании goroutine был снят с 4KB до 8KB

Перейти 1.4 Примечания к выпуску: Изменения в среде выполнения:

начальный размер по умолчанию для стека goroutine в 1.4 был уменьшен с 8192 байт до 2048 байт.

Память per-goroutine в основном состоит из стека, и она начинает низко и растет, поэтому вы можете дешево иметь много goroutines. Вы могли бы использовать меньший стартовый стек, но тогда он должен был бы расти раньше (увеличивать пространство по стоимости времени), а преимущества уменьшаются из-за сокращения блока управления. Можно исключить стек, по крайней мере, при замене (например, выполнить все выделение в куче или сохранить стек в кучу контекстного переключателя), хотя это ущемляет производительность и усложняет работу. Это возможно (как в Erlang), и означает, что вам нужен только блок управления и сохраненный контекст, позволяя получить еще один коэффициент в размере 5 × -10 × в количестве goroutines, ограниченный теперь размером блока управления и размером кучи goroutine-local переменные. Тем не менее, это не очень полезно, если вам не нужны миллионы крошечных спящих гостей.

Поскольку основное использование множества goroutines связано с задачами, связанными с IO (конкретно для обработки блокировки системных вызовов, в частности, для ввода в сеть или файловой системы IO), вы, скорее всего, столкнетесь с ограничениями ОС на другие ресурсы, а именно на сетевые сокеты или файл handle: golang-nuts > Максимальное количество goroutines и файловых дескрипторов?. Обычным способом решения этой проблемы является pool ограниченного ресурса, или, проще говоря, просто ограничивая число с помощью semaphore; см. Сохранение дескрипторов файлов в Go и Ограничение Concurrency в Go.

Ответ 3

Это полностью зависит от системы, в которой вы работаете. Но goroutines очень легкие. Средний процесс не должен иметь проблем с 100 000 одновременных подпрограмм. Является ли это для вашей целевой платформы, конечно же, мы не можем ответить, не зная, что это за платформа.

Ответ 4

Перефразируя, есть ложь, проклятая ложь и контрольные показатели. Как признался автор теста Erlang,

Само собой разумеется, что в машина, чтобы на самом деле сделать что-нибудь полезное. стресс-тестирование erlang

Какое ваше оборудование, какая у вас операционная система, где ваш исходный код теста? Каков критерий, который пытается измерить и доказать/опровергнуть?

Ответ 6

Если число goroutine когда-либо становилось проблемой, вы легко можете ограничить его для своей программы:
См. mr51m0n/gorc и этот пример.

Установите пороговые значения для числа запущенных goroutines

Может увеличивать и уменьшать счетчик при запуске или остановке goroutine.
Он может дождаться минимального или максимального числа запущенных goroutines, что позволяет установить пороговые значения для числа управляемых gotoutines gorc, работающих одновременно.