🏃 Горутины: что такое и как работают

Легковесная, потребляет мало памяти, имеет низкую задержку — знакомимся с горутиной.

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

Определение потока в программировании

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

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

Простым примером многопоточности является веб-браузер. Ведь в нем можно одновременно загружать файлы, прокручивать страницы вниз, что-то печатать и слушать музыку. Технически, поток можно назвать облегченным процессом, ведь он требует меньше памяти, чем процесс в многопроцессорной среде. Linux обычно обобщает их, называя задачами (они могут быть как процессами, так и потоками).

Однако, между ними все же есть различие.

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

Что такое Горутины?

Горутины — это дальнейшее усовершенствование концепции потока, а если сказать проще, то это функции, способные работать параллельно с другими такими же функциями в одном адресном пространстве. Причем их настолько хорошо усовершенствовали, что они стали отдельной сущностью. В многопроцессорной среде создание и обслуживание процесса сильно зависят от базовой операционной системы. Процессы потребляют ресурсы операционки и не делят их между узлами. Потоки, хотя и легче, чем процессы, из-за совместного использования ресурсов (между одноранговыми потоками), требуют большого размера стека — почти 1 МБ. Причем стек нужно умножать на количество потоков.

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

Преимущество горутин в том, что они не зависят от базовой операционной системы, а скорее, существуют в виртуальном пространстве среды выполнения Go. В результате любая оптимизация горутины меньше зависит от платформы, на которой она работает. Они начинают работать с начальной емкости стека размером всего 2-4 КБ и наряду с каналами поддерживают модели параллелизма взаимодействующих последовательных процессов (CSP), где значения передаются между независимыми действиями. Эти действия, как вы уже догадались, называются горутинами.

Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека Go разработчика»

Как создать горутину в Go

Разработчики должны понимать, что горутины превосходят потоки только количественно. Качественно они одинаковы. При запуске программы в Golang первая горутина вызывает основную функцию и из-за этого ее часто называют основной горутиной. Если же мы захотим создать другие, новые горутины, мы должны использовать оператор go. Например, чтобы вызвать функцию в Golang мы пишем так:

myfunc()

Здесь, после того как ее вызвали, мы опять вернемся к точке вызова. А теперь мы напишем:

go myfunc()

Префикс go вызывает функцию в новой горутине и она (функция) будет выполняться асинхронно с вызвавшим ее участком кода. И если примерно брать в среднем на одну горутину по 4 Кб емкости стека, то имея оперативную память 4Gb, мы сможем создать их около 800 000.

Однако злоупотреблять с ними не стоит, ведь полезны они будут только в следующих случаях:

  1. Когда нам необходима асинхронность. Например, при работе с сетью, дисками, базами данных и т. д.
  2. При большом времени выполнения функции, когда мы можем что-то выиграть, нагрузив другие ядра.

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

Из документации Go: «Переменная GOMAXPROCS ограничивает количество потоков операционной системы, которые могут одновременно выполнять код Go на уровне пользователя. Количество потоков, которые можно заблокировать в системных вызовах от имени кода Go, не ограничено.

Давайте рассмотрим простой пример для лучшего понимания работы горутин:

func main() {
	i:= 10
	go fmt.Printf("1. Значение переменной i равно %d\n", i)
	i++
	go fmt.Printf("2. Значение переменной i равно %d\n", i)
	go func() {
	i++
	go fmt.Printf("3. Значение переменной i равно %d\n", i)
	}()
	i++
	go fmt.Printf("4. Значение переменной i равно %d\n", i)
	time.Sleep(1000000)
}

Запуск приведенного выше кода в вашем редакторе приведет к следующему результату:

4. Значение переменной i равно 12
3. Значение переменной i равно 13
1. Значение переменной i равно 10
2. Значение переменной i равно 11

Ниже приведен тот же код, только без использования горутин:

func main() {
	i:= 10
	fmt.Printf("1. Значение переменной i равно %d\n", i)
	i++
	fmt.Printf("2. Значение переменной i равно %d\n", i)
	func() {
	i++
	fmt.Printf("3. Значение переменной i равно %d\n", i)
	}()
	i++
	fmt.Printf("4. Значение переменной i равно %d\n", i)
	time.Sleep(1000000)
}

Здесь ожидаемый результат будет следующим:

1. Значение переменной i равно 10
2. Значение переменной i равно 11
3. Значение переменной i равно 12
4. Значение переменной i равно 13

Когда мы ставим перед любой функцией ключевое слово go, оно создает новую горутину и планирует ее выполнение. Идея и поведение такой сущности полностью идентичны потокам: она имеет полный доступ к аргументам, глобальным переменным и остальным доступным элементам кода. Кроме того, мы можем писать горутины с анонимными функциями. Если вы удалите вызов sleep() в первом примере, вывод не будет показан. Следовательно, вызов функции в конце main приведет к тому, что основная горутина остановится и выйдет до того, как порожденная ею горутина получит какой-либо шанс произвести вывод.

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

***

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

Материалы по теме

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

Библиотека программиста
23 ноября 2018

Go vs Python: изучение основ языка Go в сравнении с Python

Это не соревнование двух языков, а просто еще один способ обучения. Рассмат...
admin
19 сентября 2018

TOП-3 языка программирования, которые нужно выучить до 2019

Это не просто три лучших языка программирования, а в некотором смысле попыт...