В этой статье я постараюсь кратко и емко рассказать что такое горутины, когда стоит их использовать, какое отношение они имеют к системным потокам, а так же как работает планировщик.
Что за горутины?
Горутина (goroutine) — это функция, выполняющаяся конкурентно с другими горутинами в том же адресном пространстве.
Запустить горутину очень просто:
go normalFunc(args...)
Функция normalFunc(args...)
начнет выполняться асинхронно с вызвавшим ее кодом.
Обратите внимание, горутины очень легковесны. Практически все расходы — это создание стека, который очень невелик, хотя при необходимости может расти.
Сколько вешать в граммах?
Чтобы было проще ориентироваться, рассмотрим цифры полученные опытным путем.
В среднем можно рассчитывать примерно на 4,5kb на горутину. То есть, например, имея 4Gb оперативной памяти, вы сможете содержать около 800 тысяч работающих горутин. Может этого и недостаточно, чтобы впечатлить любителей Erlang, но мне кажется, что цифра весьма достойная ?
И все же не стоит бездумно выделять функцию в горутину где только можно. Пользу это принесет в следующих случаях:
- Если нужна асинхронность. Например когда мы работаем с сетью, диском, базой данных, защищенным мьютексом ресурсом и т.п.
- Если время выполнения функции достаточно велико и можно получить выигрыш, нагрузив другие ядра.
«Достаточно велико» — это сколько? Сложный вопрос. Скорее всего решать придется в зависимости от конкретной ситуации. Скажу только, что из моего опыта на моем железе (atom d525 64bit) это ~50мкс. А вообще, тестируйте и развивайте чутье ?
Системные потоки
В исходном коде (src/pkg/runtime/proc.c) приняты такие термины:
G (Goroutine) — Горутина
M (Machine) — Машина
Каждая Машина работает в отдельном потоке и способна выполнять только одну Горутину в момент времени. Планировщик операционной системы, в которой работает программа, переключает Машины. Число работающих Машин ограничено переменной среды GOMAXPROCS
или функцией runtime.GOMAXPROCS(n int)
. По умолчанию оно равно 1. Обычно имеет смысл сделать его равным числу ядер.
Планировщик Go
Цель планировщика (scheduler) в том, чтобы распределять готовые к выполнению горутины (G) по свободным машинам (M).
Рисунок и описание планировщика взяты из работы Sindre Myren RT Capabillites of Google Go.
Готовые к исполнению горутины выполняются в порядке очереди, то есть FIFO (First In, First Out). Исполнение горутины прерывается только тогда, когда она уже не может выполняться: то есть из-за системного вызова или использования синхронизирующих объектов (операции с каналами, мьютексами и т.п.). Не существует никаких квантов времени на работу горутины, после выполнения которых она бы заново возвращалась в очередь. Чтобы позволить планировщику сделать это, нужно самостоятельно вызвать runtime.Gosched()
.
Как только функция вновь готова к выполнению, она снова попадает в очередь.
Выводы
Скажу сразу, что меня текущее положение дел сильно удивило. Почему-то подсознательно я ожидал большей «параллельности», наверное потому что воспринимал горутины как легковесные системные потоки. Как видите, это не совсем так.
На практике это в первую очередь означает, что иногда стоит использовать runtime.Gosched()
, чтобы несколько долгоживущих горутин не остановили на существенное время работу всех других. С другой стороны, такие ситуации встречаются на практике довольно редко.
Так же хочу отметить, что на данный момент планировщик отнюдь не совершенен, о чем упоминается в документации и официальном FAQ. В будущем планируются серьезные улучшения. Лично я в первую очередь ожидаю возможность задания горутинам приоритета.