GoSuda

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

By hamori
views ...

Goroutine

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

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

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

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

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

Часто используемый Multi thread в Java, имеющий высокую загруженность, является типичным примером Parallelism.

Чем хороша Goroutine?

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

Стоимость создания очень низка по сравнению с другими языками. Возникает вопрос: почему golang использует их так мало? Это потому, что место создания управляется внутри среды выполнения Go. Это связано с тем, что это легковесный логический поток, который меньше, чем единица потока ОС, требует начального стека размером около 2 КБ и динамически изменяется путем добавления стека в соответствии с реализацией пользователя.

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

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

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

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

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

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

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

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

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

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

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

Хотя я говорил о преимуществах Concurrency goroutine, у вас может возникнуть вопрос, не поддерживает ли она Parallelism? Это связано с тем, что количество ядер CPU в последнее время превышает двузначные числа, и даже в домашних ПК количество ядер немалое.

Однако 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 имеет очень важное значение. Это связано с тем, что повысилась применимость языка в контейнерных средах. Благодаря этому удалось сократить ненужное создание потоков и переключение контекста, предотвращая CPU throttling.

 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) // Goroutine %d: начало
14	time.Sleep(10 * time.Millisecond) // 작업 시뮬레이션을 위한 지연 // Задержка для симуляции работы
15	fmt.Printf("Goroutine %d: 시작\n", name) // Goroutine %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)

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

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

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

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

  1. При создании G (Goroutine) он назначается в локальную очередь выполнения 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 (Processor), чтобы все 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.

Спасибо.