GoSuda

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

By hamori
views ...

Goroutine

Когда просишь Gopher-ов рассказать о преимуществах Golang, часто упоминается Concurrency (параллелизм). Основой этого является легковесная и простая в обработке goroutine. Я кратко изложил информацию о ней.

Concurrency vs. Parallelism

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

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

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

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

Почему goroutine хороша?

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

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

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

Высокая производительность (performance)

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

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

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

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

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

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

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

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

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

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

Однако 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.

 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, используемой для выделения и управления OS thread, стоит более подробно рассмотреть модель GMP goroutine.

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

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 во время ожидания операций ввода-вывывода.
  6. Если одна G (Goroutine) долго удерживает приоритет (preempted), она дает шанс на выполнение другой G (Goroutine).

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

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

Спасибо.