[Программирование, Go] Что такое горутины и каков их размер? (перевод)
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
В преддверии старта курса Golang Developer. Professional приглашаем всех желающих на бесплатный демо-урок по теме: "Интеграционные тесты на Go".
А сейчас традиционно публикуем полезный перевод.
Я почти уверен, что любой, кто изучал когда-либо Go, слышал, что «горутины похожи на легковесные потоки» и что «можно спокойно запускать сотни и тысячи горутин». Некоторые люди узнавали, что «горутина весит около 2 килобайт», скорее всего, из примечаний к релизу Go 1.4, и еще меньше людей узнавали, что это изначальный размер ее стека.Все эти утверждения верны, и я хочу продемонстрировать почему это так, исследовать с вами что такое горутина, сколько места она занимает, и наметить отправные точки для всех, кто интересуется внутренним устройством Go.В рамках этого исследования я буду использовать ветку Go 1.14, поэтому все фрагменты кода будут ссылаться на нее.Планировщик горутинПланировщик горутин (Goroutine scheduler) является перехватывающим задачи (work-stealing) планировщиком, который был введен еще в Go 1.1 Дмитрием Вьюковым вместе с командой Go. Его диздок доступен здесь и включает рассуждения на тему возможных будущих улучшений. Существует множество замечательных ресурсов, помогающих разобраться, как он работает, но основная суть заключается в том, что он пытается управлять G, M и P; горутинами, машинами (потоками) и процессорами.«G» - это просто горутина Golang.«M» - это поток ОС, который может выполнять что-либо или же бездействовать.«P» можно рассматривать как ЦП в планировщике ОС; он представляет ресурсы, необходимые для выполнения нашего Go кода, такие как планировщик или состояние распределителя памяти.В рантайме они представлены как структуры: type g, type m или type p.Основная задача планировщика состоит в том, чтобы сопоставить каждую G (код, который мы хотим выполнить) с M (где его выполнять) и P (права и ресурсы для выполнения).Когда M прекращает выполнение нашего кода, он возвращает свой P в пул свободных P. Чтобы возобновить выполнение Go кода, он должен повторно заполучить его. Точно так же, когда горутина завершается, объект G возвращается в пул свободных G и позже может быть повторно использован для какой-либо другой горутины.Чтобы запустить горутину, запускающую на выполнение main либо из кода, структура g инициализируется с помощью функции malg
//Выделяет новую g со стеком, достаточно большим для stacksize байтов.
func malg(stacksize int32) *g {
newg := new(g) // <--- все начинается здесь
if stacksize >= 0 {
stacksize = round2(_StackSystem + stacksize)
systemstack(func() {
newg.stack = stackalloc(uint32(stacksize))
})
newg.stackguard0 = newg.stack.lo + _StackGuard
newg.stackguard1 = ^uintptr(0)
...
...
}
return newg
}
которая вызывается из newproc и newproc1.
// Создаем новую g, выполняющую fn с narg байтами аргументов, начинающихся с argp. callerpc - это адрес оператора go, который ее создал. Новая g помещается в очередь g, ожидающих запуска.
func newproc1(fn *funcval, argp unsafe.Pointer, narg int32, callergp *g, callerpc uintptr) {
...
acquirem() // отключаем вытеснение менее приоритетных задач, потому что оно может удерживать p в локальном var
siz := narg
siz = (siz + 7) &^ 7
...
_p_ := _g_.m.p.ptr()
newg := gfget(_p_)
if newg == nil {
newg = malg(_StackMin) // !!! <- здесь происходит магия
casgstatus(newg, _Gidle, _Gdead)
allgadd(newg)
}
...
}
Итак, теперь мы готовы проанализировать саму горутину!Объект горутины Длина объекта горутины составляет порядка 70 строк. Позвольте мне удалить комментарии и навести небольшой порядок:
type g struct {
stack stack
stackguard0 uintptr
stackguard1 uintptr
_panic *_panic
_defer *_defer
m *m
sched gobuf
syscallsp uintptr
syscallpc uintptr
stktopsp uintptr
param unsafe.Pointer
atomicstatus uint32
stackLock uint32
goid int64
schedlink guintptr
waitsince int64
waitreason waitReason
preempt bool
preemptStop bool
preemptShrink bool
asyncSafePoint bool
paniconfault bool
gcscandone bool
throwsplit bool
activeStackChans bool
raceignore int8
sysblocktraced bool
sysexitticks int64
traceseq uint64
tracelastp puintptr
lockedm muintptr
sig uint32
writebuf []byte
sigcode0 uintptr
sigcode1 uintptr
sigpc uintptr
gopc uintptr
ancestors *[]ancestorInfo
startpc uintptr
racectx uintptr
waiting *sudog
cgoCtxt []uintptr
labels unsafe.Pointer
timer *timer
selectDone uint32
gcAssistBytes int64
}
Вот и все!Попробуем подсчитать суммарный размер; uintptr весит 64 бит, т.е. 8 байт в нашей архитектуре, так же как и int64. Логические значения имеют размер в 1 байт, а слайс - это просто указатель плюс два инта.Есть более сложные типы, такие как timer (~70 байт), _panic (~40 байт) или _defer (~100 байт), но в целом я насчитал около 600 байт.Хм, это кажется немного подозрительным… Откуда взялось пресловутое значение «2 кб»?Давайте подробнее рассмотрим первое поле структуры…Стек горутиныПервое поле структуры g имеет тип stack.
type g struct {
// Параметры стека.
// stack описывает фактическую память стека: [stack.lo, stack.hi).
// stackguard0 - указатель стека, сравниваемый по мере роста стека Go.
// stackguard1 - указатель стека, сравниваемый по мере роста стека C.
...
stack stack // смещение, известное runtime/cgo
stackguard0 uintptr // смещение, известное liblink
stackguard1 uintptr // смещение, известное liblink
Сам стек представляет собой не что иное, как два значения, обозначающих его начало и конец.
type stack struct {
lo uintptr
hi uintptr
}
К этому времени вы, вероятно, зададитесь вопросом: «А каков же размер этого стека?», или уже догадаетесь, что 2 килобайта относятся к этому стеку!Горутина стартует с минимального размера стека в 2 килобайта, который увеличивается и уменьшается по мере необходимости без риска когда-либо закончиться.Эта отличная статья Дэйва Чейни более подробно объясняет, как это работает. По сути, перед выполнением любой функции Go проверяет, доступен ли объем стека, необходимый для функции, которую он собирается выполнить; если нет, то выполняется вызов runtime.morestack, который выделяет новую страницу, и только после этого выполняется функция. Наконец, когда эта функция завершается, ее возвращаемые аргументы копируются обратно в исходный фрейм стека, а все невостребованное пространство стека высвобождается.Хотя минимальный размер стека определен как 2048 байтов, рантайм Go также не позволяет горутинам превышать максимальный размер стека; этот максимум зависит от архитектуры и составляет 1 ГБ для 64-разрядных систем и 250 МБ для 32-разрядных систем.Если этот предел достигнут, будет выполнен вызов runtime.abort. Превысить этого размер очень просто с помощью рекурсивной функции; все, что вам нужно сделать, это
package main
func foo(i int) int {
if i < 1e8 {
return foo(i + 1)
}
return -1
}
func main() {
foo(0)
}
И мы видим, что приложение паникует, стек больше не может расти и выполняется вышеупомянутый runtime.abort .
$ go run exceed-stack.go
runtime: goroutine stack exceeds 1000000000-byte limit
fatal error: stack overflow
runtime stack:
runtime.throw(0x1071ce1, 0xe)
/usr/local/go/src/runtime/panic.go:774 +0x72
runtime.newstack()
/usr/local/go/src/runtime/stack.go:1046 +0x6e9
runtime.morestack()
/usr/local/go/src/runtime/asm_amd64.s:449 +0x8f
goroutine 1 [running]:
main.foo(0xffffdf, 0x0)
...
...
Итак, сколько горутин вы можете запустить?Я использую скрипт из Приложения к статье, скопированный отсюда.На ноутбуке среднего уровня я могу запустить 50 миллионов горутин.По мере роста числа возникают две основные проблемы: использование памяти (и начинается свопинг) и более медленная сборка мусора.
$ ~ go run poc-goroutines-sizing.go
# 10 Тысячь горутин
Number of goroutines: 100000
Per goroutine:
Memory: 2115.71 bytes
Time: 1.404500 µs
# 1 Миллион горутин
Number of goroutines: 1000000
Per goroutine:
Memory: 2655.21 bytes
Time: 1.518857 µs
# 3 Миллиона горутин
Number of goroutines: 3000000
Per goroutine:
Memory: 2700.37 bytes
Time: 1.637003 µs
# 6 Миллионов горутин
Number of goroutines: 6000000
Per goroutine:
Memory: 2700.29 bytes
Time: 2.541744 µs
# 9 Миллионов горутин
Number of goroutines: 9000000
Per goroutine:
Memory: 2700.27 bytes
Time: 2.857699 µs
# 12 Миллионов горутин
Number of goroutines: 12000000
Per goroutine:
Memory: 2694.09 bytes
Time: 3.232870 µs
# 50 Миллионов горутин
Number of goroutines: 50000000
Per goroutine:
Memory: 2695.37 bytes
Time: 5.098005 µs
ЗаключениеНа этом пожалуй все!Есть планировщик горутин, с помощью которого код Go планируется для запуска на хосте. Затем есть сами горутины, в которых фактически выполняется код Go, и есть стек каждой горутины, который увеличивается и уменьшается, чтобы приспособиться к выполнению кода.Я рекомендую бегло просмотреть src/runtime/HACKING.md, где многие концепции и соглашения кода в среде выполнения Golang объясняются более подробно.Надеюсь, вы узнали что-то новое и у вас появились кое-какие отправные точки, чтобы разобраться в коде самого языка Go.До встречи!Ссылки
- https://stackoverflow.com/questions/8509152/max-number-of-goroutines
- https://medium.com/a-journey-with-go/go-how-does-th...lve-447fc02085e5
- https://dave.cheney.net/2013/06/02/why-is-a-goroutines-stack-infinite
- https://www.ardanlabs.com/blog/2018/08/scheduling-in-go-part1.html
- https://medium.com/@genchilu/if-a-goroutine-call-a-...rst-890002dc54f8
- https://povilasv.me/go-scheduler/
Приложение
package main
import (
"flag"
"fmt"
"os"
"runtime"
"time"
)
var n = flag.Int("n", 3*1e6, "Number of goroutines to create")
var ch = make(chan byte)
var counter = 0
func f() {
counter++
<-ch// Блокируем эту горутину
}
func main() {
flag.Parse()
if *n <= 0 {
fmt.Fprintf(os.Stderr, "invalid number of goroutines")
os.Exit(1)
}
// Ограничиваем количество свободных потоков ОС до 1
runtime.GOMAXPROCS(1)
// Делаем копию 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()
// Делаем копию 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)
}
Записаться на бесплатный демо-урок
===========
Источник:
habr.com
===========
===========
Автор оригинала: tpaschalis
===========Похожие новости:
- [Программирование, Машинное обучение] Машинное обучение с Dask (перевод)
- [Ненормальное программирование, Программирование, Rust] Пишем ОС на Rust. Настройка среды. Бинарник для «голого» железа
- [Разработка веб-сайтов, Программирование, Конференции, Flutter] DartUP 2020: в этом году онлайн и с известными спикерами
- [Open source, Программирование, C++] Новая функциональность в RESTinio и опять с помощью C++ных шаблонов
- [Node.JS, Google API, Монетизация мобильных приложений] Разворачиваем сервер для проверки In-app purchase за 60 минут
- [Хранилища данных, Фототехника] Google Photos больше не будет безлимитным
- [Python, Data Engineering] Python API в Delta Lake — простые и надежные операции Upsert и Delete (перевод)
- [Космонавтика, Будущее здесь] НАСА сертифицировало ракету Falcon 9 и корабль Crew Dragon от SpaceX для выполнения регулярных космических полетов
- [Программирование, Разработка под iOS, Swift] Разница между @StateObject, @EnvironmentObject и @ObservedObject в SwiftUI (перевод)
- [Python, Программирование, Машинное обучение] Быстрый градиентный бустинг с CatBoost (перевод)
Теги для поиска: #_programmirovanie (Программирование), #_go, #_go, #_golang, #_goroutine, #_blog_kompanii_otus._onlajnobrazovanie (
Блог компании OTUS. Онлайн-образование
), #_programmirovanie (
Программирование
), #_go
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 23-Ноя 01:57
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
В преддверии старта курса Golang Developer. Professional приглашаем всех желающих на бесплатный демо-урок по теме: "Интеграционные тесты на Go".
А сейчас традиционно публикуем полезный перевод. Я почти уверен, что любой, кто изучал когда-либо Go, слышал, что «горутины похожи на легковесные потоки» и что «можно спокойно запускать сотни и тысячи горутин». Некоторые люди узнавали, что «горутина весит около 2 килобайт», скорее всего, из примечаний к релизу Go 1.4, и еще меньше людей узнавали, что это изначальный размер ее стека.Все эти утверждения верны, и я хочу продемонстрировать почему это так, исследовать с вами что такое горутина, сколько места она занимает, и наметить отправные точки для всех, кто интересуется внутренним устройством Go.В рамках этого исследования я буду использовать ветку Go 1.14, поэтому все фрагменты кода будут ссылаться на нее.Планировщик горутинПланировщик горутин (Goroutine scheduler) является перехватывающим задачи (work-stealing) планировщиком, который был введен еще в Go 1.1 Дмитрием Вьюковым вместе с командой Go. Его диздок доступен здесь и включает рассуждения на тему возможных будущих улучшений. Существует множество замечательных ресурсов, помогающих разобраться, как он работает, но основная суть заключается в том, что он пытается управлять G, M и P; горутинами, машинами (потоками) и процессорами.«G» - это просто горутина Golang.«M» - это поток ОС, который может выполнять что-либо или же бездействовать.«P» можно рассматривать как ЦП в планировщике ОС; он представляет ресурсы, необходимые для выполнения нашего Go кода, такие как планировщик или состояние распределителя памяти.В рантайме они представлены как структуры: type g, type m или type p.Основная задача планировщика состоит в том, чтобы сопоставить каждую G (код, который мы хотим выполнить) с M (где его выполнять) и P (права и ресурсы для выполнения).Когда M прекращает выполнение нашего кода, он возвращает свой P в пул свободных P. Чтобы возобновить выполнение Go кода, он должен повторно заполучить его. Точно так же, когда горутина завершается, объект G возвращается в пул свободных G и позже может быть повторно использован для какой-либо другой горутины.Чтобы запустить горутину, запускающую на выполнение main либо из кода, структура g инициализируется с помощью функции malg //Выделяет новую g со стеком, достаточно большим для stacksize байтов.
func malg(stacksize int32) *g { newg := new(g) // <--- все начинается здесь if stacksize >= 0 { stacksize = round2(_StackSystem + stacksize) systemstack(func() { newg.stack = stackalloc(uint32(stacksize)) }) newg.stackguard0 = newg.stack.lo + _StackGuard newg.stackguard1 = ^uintptr(0) ... ... } return newg } // Создаем новую g, выполняющую fn с narg байтами аргументов, начинающихся с argp. callerpc - это адрес оператора go, который ее создал. Новая g помещается в очередь g, ожидающих запуска.
func newproc1(fn *funcval, argp unsafe.Pointer, narg int32, callergp *g, callerpc uintptr) { ... acquirem() // отключаем вытеснение менее приоритетных задач, потому что оно может удерживать p в локальном var siz := narg siz = (siz + 7) &^ 7 ... _p_ := _g_.m.p.ptr() newg := gfget(_p_) if newg == nil { newg = malg(_StackMin) // !!! <- здесь происходит магия casgstatus(newg, _Gidle, _Gdead) allgadd(newg) } ... } type g struct {
stack stack stackguard0 uintptr stackguard1 uintptr _panic *_panic _defer *_defer m *m sched gobuf syscallsp uintptr syscallpc uintptr stktopsp uintptr param unsafe.Pointer atomicstatus uint32 stackLock uint32 goid int64 schedlink guintptr waitsince int64 waitreason waitReason preempt bool preemptStop bool preemptShrink bool asyncSafePoint bool paniconfault bool gcscandone bool throwsplit bool activeStackChans bool raceignore int8 sysblocktraced bool sysexitticks int64 traceseq uint64 tracelastp puintptr lockedm muintptr sig uint32 writebuf []byte sigcode0 uintptr sigcode1 uintptr sigpc uintptr gopc uintptr ancestors *[]ancestorInfo startpc uintptr racectx uintptr waiting *sudog cgoCtxt []uintptr labels unsafe.Pointer timer *timer selectDone uint32 gcAssistBytes int64 } type g struct {
// Параметры стека. // stack описывает фактическую память стека: [stack.lo, stack.hi). // stackguard0 - указатель стека, сравниваемый по мере роста стека Go. // stackguard1 - указатель стека, сравниваемый по мере роста стека C. ... stack stack // смещение, известное runtime/cgo stackguard0 uintptr // смещение, известное liblink stackguard1 uintptr // смещение, известное liblink type stack struct {
lo uintptr hi uintptr } package main
func foo(i int) int { if i < 1e8 { return foo(i + 1) } return -1 } func main() { foo(0) } $ go run exceed-stack.go
runtime: goroutine stack exceeds 1000000000-byte limit fatal error: stack overflow runtime stack: runtime.throw(0x1071ce1, 0xe) /usr/local/go/src/runtime/panic.go:774 +0x72 runtime.newstack() /usr/local/go/src/runtime/stack.go:1046 +0x6e9 runtime.morestack() /usr/local/go/src/runtime/asm_amd64.s:449 +0x8f goroutine 1 [running]: main.foo(0xffffdf, 0x0) ... ... $ ~ go run poc-goroutines-sizing.go
# 10 Тысячь горутин Number of goroutines: 100000 Per goroutine: Memory: 2115.71 bytes Time: 1.404500 µs # 1 Миллион горутин Number of goroutines: 1000000 Per goroutine: Memory: 2655.21 bytes Time: 1.518857 µs # 3 Миллиона горутин Number of goroutines: 3000000 Per goroutine: Memory: 2700.37 bytes Time: 1.637003 µs # 6 Миллионов горутин Number of goroutines: 6000000 Per goroutine: Memory: 2700.29 bytes Time: 2.541744 µs # 9 Миллионов горутин Number of goroutines: 9000000 Per goroutine: Memory: 2700.27 bytes Time: 2.857699 µs # 12 Миллионов горутин Number of goroutines: 12000000 Per goroutine: Memory: 2694.09 bytes Time: 3.232870 µs # 50 Миллионов горутин Number of goroutines: 50000000 Per goroutine: Memory: 2695.37 bytes Time: 5.098005 µs
package main
import ( "flag" "fmt" "os" "runtime" "time" ) var n = flag.Int("n", 3*1e6, "Number of goroutines to create") var ch = make(chan byte) var counter = 0 func f() { counter++ <-ch// Блокируем эту горутину } func main() { flag.Parse() if *n <= 0 { fmt.Fprintf(os.Stderr, "invalid number of goroutines") os.Exit(1) } // Ограничиваем количество свободных потоков ОС до 1 runtime.GOMAXPROCS(1) // Делаем копию 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() // Делаем копию 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) } =========== Источник: habr.com =========== =========== Автор оригинала: tpaschalis ===========Похожие новости:
Блог компании OTUS. Онлайн-образование ), #_programmirovanie ( Программирование ), #_go |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 23-Ноя 01:57
Часовой пояс: UTC + 5