GoSuda

Основы горутин

By hamori
views ...

Goroutine

Если попросить гоферов рассказать о преимуществах golang, часто можно встретить статьи о параллелизме (Concurrency). Основой этого является легковесная и простая в обработке горутина (goroutine). Ниже представлено краткое изложение этого понятия.

Параллелизм (Concurrency) vs. Многопоточность (Parallelism)

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

  • Параллелизм (Concurrency): Параллелизм (Concurrency) связан с одновременной обработкой множества задач. Это не обязательно означает, что они выполняются буквально одновременно. Это структурная, логическая концепция, при которой несколько задач делятся на мелкие единицы и выполняются поочередно, создавая у пользователя иллюзию одновременной обработки нескольких задач. Параллелизм (Concurrency) возможен даже на одноядерном процессоре.
  • Многопоточность (Parallelism): Многопоточность (Parallelism) — это "одновременное выполнение нескольких задач на нескольких ядрах". Это буквально параллельное выполнение работы, когда различные задачи выполняются одновременно.

Горутины позволяют легко реализовать параллелизм (Concurrency) через планировщик времени выполнения Go, а также естественным образом использовать многопоточность (Parallelism) с помощью настройки GOMAXPROCS.

Многопоточность (Multi thread) в Java, которая часто используется и имеет высокую степень утилизации, является типичным примером концепции многопоточности (Parallelism).

Почему горутины хороши?

Легковесность (lightweight)

Стоимость создания очень низка по сравнению с другими языками. Возникает вопрос: почему golang использует меньше ресурсов? Ответ заключается в том, что место создания управляется внутри среды выполнения Go (Go runtime). Это объясняется тем, что горутина является вышеупомянутым легковесным логическим потоком. Она меньше единицы OS-потока, требует начального стека размером около 2 КБ и может динамически изменяться в размере путем добавления стека в зависимости от реализации пользователя.

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

Производительность (performance)

Прежде всего, как было объяснено выше, Goroutine требует меньшего вмешательства ядра OS, что делает переключение контекста на уровне пользователя (User-Level) менее затратным, чем на уровне OS-потоков, и позволяет быстро переключать задачи.

Кроме того, он управляется путем выделения OS-потокам с использованием модели M:N. Это позволяет обрабатывать задачи с меньшим количеством потоков, создавая пул OS-потоков, без необходимости в большом количестве потоков. Например, если горутина переходит в состояние ожидания, такое как системный вызов, среда выполнения Go запускает другую горутину на OS-потоке, что позволяет эффективно использовать CPU без простоя OS-потока и обеспечивает быструю обработку.

Благодаря этому Golang может показывать более высокую производительность в операциях I/O по сравнению с другими языками.

Лаконичность (concise)

Возможность легко обрабатывать функцию с помощью одного ключевого слова go, когда требуется параллелизм (Concurrency), также является большим преимуществом.

Приходится использовать сложные механизмы блокировки, такие как Mutex, Semaphore, и обязательно учитывать состояние DeadLock, которое неизбежно возникает при использовании блокировок, что требует сложных этапов, начиная с этапа проектирования перед разработкой.

Goroutine придерживается философии "Не обменивайтесь данными, совместно используя память; совместно используйте память, обмениваясь данными" и рекомендует передачу данных через Channel. SELECT в сочетании с Channel поддерживает функцию, позволяющую обрабатывать каналы, данные в которых готовы. Кроме того, использование sync.WaitGroup позволяет легко управлять потоком работы, просто ожидая завершения всех горутин. Благодаря этим инструментам можно предотвратить проблемы конкуренции данных между потоками и обеспечить более безопасную обработку параллелизма (Concurrency).

Кроме того, использование context позволяет контролировать жизненный цикл, отмену, тайм-аут, дедлайн и область запроса на уровне пользователя (User-Level), что обеспечивает определенную степень стабильности.

Параллельная работа Goroutine (GOMAXPROCS)

Возможно, возникнет вопрос: раз мы говорили о преимуществах параллелизма (Concurrency) goroutine, поддерживает ли она многопоточность (Parallelism)? В последнее время количество ядер CPU превышает десятки, в отличие от прошлого, и даже в домашних PC используется немалое количество ядер.

Однако Goroutine выполняет и параллельную работу, и это GOMAXPROCS.

Если GOMAXPROCS не установлен, он настраивается по-разному в зависимости от версии.

  1. До 1.5: Значение по умолчанию 1. Если требуется больше 1, необходимо настроить с помощью runtime.GOMAXPOCS(runtime.NumCPU()).

  2. 1.5 ~ 1.24: Изменено на все доступные логические ядра. С этого момента разработчику не нужно настраивать, если это не требуется в особых случаях.

  3. 1.25: Как язык, известный в контейнерных средах, он проверяет cGroup в linux и определяет ограничение CPU, установленное для контейнера.

    Тогда, если количество логических ядер равно 10, а ограничение CPU равно 5, GOMAXPROCS устанавливается на меньшее число — 5.

Изменение в 1.25 является очень значительным, поскольку повысилась полезность языка в контейнерных средах. Это позволило уменьшить ненужное создание потоков и переключение контекста, что предотвращает троттлинг (throttling) CPU.

 1package main
 2
 3import (
 4	"fmt"
 5	"math/rand"
 6	"runtime"
 7	"time"
 8)
 9
10func exe(name int, wg *sync.WaitGroup) {
11	defer wg.Done()
12
13	fmt.Printf("Goroutine %d: 시작\n", name) // Горутина %d: начало
14	time.Sleep(10 * time.Millisecond) // 작업 시뮬레이션을 위한 지연 // Задержка для симуляции работы
15	fmt.Printf("Goroutine %d: 시작\n", name) // Горутина %d: начало
16}
17
18func main() {
19	runtime.GOMAXPROCS(2) // CPU 코어 2개만 사용 // Использовать только 2 ядра CPU
20	wg := sync.WaitGroup();
21  goroutineCount := 10
22	wg.Add(goroutineCount)
23
24	for i := 0; i < goroutineCount; i++ {
25		go exe(i, &wg)
26	}
27
28	fmt.Println("모든 goroutine이 끝날 때까지 대기합니다...") // Ожидание завершения всех goroutine...
29	wg.Wait()
30	fmt.Println("모든 작업이 완료되었습니다.") // Все работы завершены.
31
32}
33

Планировщик Goroutine (Модель M:N)

В предыдущем разделе, где говорилось об управлении путем выделения OS-потокам с использованием модели M:N, мы рассмотрим более подробно модель GMP goroutine.

  • G (Goroutine): Наименьшая единица работы, выполняемая в Go.
  • M (Machine): OS-поток (фактическое место выполнения работы).
  • P (Processor): Логический процессор, управляемый средой выполнения Go.

P дополнительно имеет локальную очередь выполнения (Local Run Queue) и выполняет роль планировщика, назначающего выделенные G на M. В общих чертах, goroutine...

Процесс работы GMP выглядит следующим образом:

  1. Когда создается G (Gorutine), он назначается в локальную очередь выполнения P (Processor).
  2. P (Processor) назначает G (Goroutine) из локальной очереди выполнения на M (Machine).
  3. M (Machine) возвращает состояние G (Goroutine): block, complete или preempted.
  4. Work-Stealing (Кража работы): Если локальная очередь выполнения P становится пустой, другой P проверяет глобальную очередь. Если и там нет G (Goroutine), он "крадет" работу у другого локального P (Processsor), чтобы все M продолжали работать без простоя.
  5. Обработка системного вызова (Blocking): Если G (Goroutine) блокируется во время выполнения, M (Machine) переходит в состояние ожидания. В этот момент P (Processor) отделяется от заблокированного M (Machine) и объединяется с другим M (Machine) для выполнения следующего G (Goroutine). При этом не происходит потери CPU даже во время ожидания операций I/O.
  6. Если одна G (Goroutine) занимает процессор слишком долго (preempted), она дает возможность выполнения другой G (Goroutine).

Сборщик мусора (GC) в Golang также выполняется на Goroutine, что позволяет параллельно очищать память с минимальной приостановкой выполнения приложения (STW), эффективно используя системные ресурсы.

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

Спасибо.