31 июля 2022

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

Веб-разработчик, фрилансер... Пишу об ИТ и смежных технологиях.
Легковесная, потребляет мало памяти, имеет низкую задержку — знакомимся с горутиной.
🏃 Горутины: что такое и как работают

Язык 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. Более подробную информацию по их грамотному использованию можно получить на официальном сайте языка.

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

Комментарии

ВАКАНСИИ

Добавить вакансию
Разработчик C++
Москва, по итогам собеседования

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