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

Что за горутины?

Горутина (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. В будущем планируются серьезные улучшения. Лично я в первую очередь ожидаю возможность задания горутинам приоритета.