GoSuda

Основи на Go Routines

By hamori
views ...

Goroutine

Когато молят Gopher-ите да обяснят предимствата на golang, често се появяват статии, свързани с едновременност (Concurrency). В основата на това е горутината (goroutine), която може да бъде обработена леко и просто. Написах кратък преглед по този въпрос.

Едновременност (Concurrency) срещу Паралелност (Parallelism)

Преди да разберем горутините, бих искал първо да изясня две често бъркани понятия.

  • Едновременност: Едновременността е свързана с обработката на много задачи наведнъж. Това не означава непременно, че те се изпълняват едновременно, а по-скоро е структурна и логическа концепция, при която няколко задачи се разделят на малки единици и се изпълняват последователно, така че за потребителя изглежда, че няколко задачи се обработват едновременно. Едновременност е възможна дори и при едноядрен процесор.
  • Паралелност: Паралелността е "обработката на множество задачи едновременно на множество ядра". Тя буквално включва паралелно изпълнение на задачи и едновременно изпълнение на различни задачи.

Goroutine улеснява реализирането на едновременност чрез Go runtime scheduler и естествено използва паралелност чрез настройката GOMAXPROCS.

Multi-threading в Java, която често се използва, е типичен пример за паралелност.

Защо Goroutine е добър?

Лек (lightweight)

Разходите за създаване са много по-ниски в сравнение с други езици. Тук възниква въпросът защо golang използва по-малко ресурси? Това е така, защото мястото за създаване се управлява вътрешно от Go runtime. Причината за това е, че е лека логическа нишка, по-малка от единица OS нишка, изискваща начален стек от около 2KB, който динамично се променя чрез добавяне на стек според имплементацията на потребителя.

Тъй като се управлява на стекове, създаването и премахването са много бързи и евтини, позволявайки обработка на милиони горутини без значително натоварване. В резултат на това Goroutine може да минимизира намесата на OS ядрото благодарение на runtime scheduler.

Добра производителност (performance)

На първо място, както е обяснено по-горе, Goroutine изисква по-малко намеса на OS ядрото, така че превключването на контекста на потребителско ниво е по-евтино от това на ниво OS нишка, което позволява бързо превключване на задачите.

Освен това, той използва M:N модел за разпределение и управление на OS нишки. Чрез създаване на OS нишков пул е възможна обработка с малко нишки, без да са необходими много. Например, ако възникне състояние на изчакване като системен извикване, Go runtime изпълнява друга горутина на OS нишката, така че OS нишката не остава бездейна и ефективно използва процесора за бърза обработка.

В резултат на това Golang може да постигне по-висока производителност в I/O операции в сравнение с други езици.

Сбит (concise)

Голямо предимство е и възможността лесно да се обработват функции с единствена ключова дума go, когато е необходима едновременност.

Необходимо е да се използват сложни механизми за заключване като Mutex, Semaphore и неизбежно трябва да се вземе предвид състоянието на deadlock, което изисква сложни стъпки още от етапа на проектиране преди разработката.

Goroutine насърчава предаването на данни чрез Channel в съответствие с философията "не споделяйте памет чрез комуникация, а комуникирайте чрез споделяне на памет". SELECT в комбинация с Channel поддържа функцията за обработка на данни от първия готов канал. Освен това, sync.WaitGroup позволява лесно изчакване, докато всички горутини приключат, което улеснява управлението на работния поток. Благодарение на тези инструменти, проблемите с конкуренцията на данни между нишките се избягват и едновременната обработка става по-сигурна.

Освен това, използвайки context, може да се контролира жизнения цикъл, отмяната, времето за изчакване, крайния срок и обхвата на заявките на потребителско ниво, като по този начин се гарантира определена степен на стабилност.

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

Говорихме за предимствата на едновременността на goroutine, но вероятно се чудите дали не поддържа и паралелност. Броят на ядрата на съвременните процесори надхвърля двуцифрено число, за разлика от миналото, а дори домашните компютри имат значителен брой ядра.

Въпреки това, Goroutine извършва и паралелна работа, което е GOMAXPROCS.

Ако GOMAXPROCS не е настроен, той се задава по различен начин в зависимост от версията.

  1. Преди 1.5: По подразбиране е 1. При необходимост от повече от 1, задължително е да се настрои чрез runtime.GOMAXPOCS(runtime.NumCPU()) или подобен метод.

  2. 1.5 ~ 1.24: Променено е да използва всички налични логически ядра. От този момент нататък разработчиците нямат нужда да го настройват, освен ако не е абсолютно необходимо.

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

    Тогава, ако броят на логическите ядра е 10, а ограничението на процесора е 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) // Използвай само 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 модел, ще разгледаме по-конкретно модела goroutine GMP.

  • G (Goroutine): Най-малката единица работа, изпълнявана в Go
  • M (Machine): OS нишка (действително място на изпълнение)
  • 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): Ако възникне Block по време на изпълнение на G (Goroutine), M (Machine) преминава в състояние на изчакване. В този момент P (Processor) се отделя от блокираното M (Machine) и се свързва с друго M (Machine), за да изпълни следващото G (Goroutine). През това време няма загуба на CPU дори по време на изчакване при I/O операции.
  6. Ако една G (Goroutine) заема прекалено дълго (preempted), се дава възможност за изпълнение на други G (Goroutine).

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

И накрая, Golang е едно от силните предимства на езика, а има и много други, така че се надявам много разработчици да се насладят на Go.

Благодаря ви.