Основи на Go Routines
Goroutine
Ако помолите Gopher-ите да разкажат за предимствата на golang, често ще се появи статия, свързана с едновременността (Concurrency). Основата на това съдържание е горутината (goroutine), която е лека и лесна за обработка. Написах кратко изложение по този въпрос.
Едновременност (Concurrency) срещу Паралелизъм (Parallelism)
Преди да разберем горутината, бих искал първо да изясня две понятия, които често се бъркат.
- Едновременност (Concurrency): Едновременността се отнася до обработката на много задачи наведнъж. Това не означава непременно, че те се изпълняват едновременно, а е структурна, логическа концепция, която кара потребителя да вижда, че множество задачи се обработват едновременно, като се разделят на малки единици и се изпълняват последователно. Едновременността е възможна дори на едно ядро (single core).
- Паралелизъм (Parallelism): Паралелизмът е "едновременната обработка на множество задачи на множество ядра". Буквално означава извършване на работа паралелно и изпълнение на различни задачи едновременно.
Горутината позволява лесно да се реализира едновременност чрез Go runtime scheduler и естествено използва паралелизъм чрез настройката GOMAXPROCS
.
Многонишковата обработка (Multi thread) на Java, която често се използва, е типична концепция за паралелизъм.
Защо горутината е добра?
Лека (lightweight)
Разходите за създаване са много ниски в сравнение с други езици. Тук възниква въпросът защо golang използва малко ресурси? Това е така, защото мястото на създаване се управлява вътре в Go runtime. Причината е, че това е гореспоменатата лека логическа нишка, която е по-малка от единица OS thread, изисква начален стек с размер около 2KB и е динамично променлива чрез добавяне на стек според реализацията на потребителя.
Управлява се на ниво стек, което прави създаването и премахването много бързо и евтино, позволявайки обработка без натоварване дори при милиони горутини. В резултат на това Goroutine може да минимизира намесата на OS kernel благодарение на runtime scheduler-а.
Висока производителност (performance)
Първо, както беше обяснено по-горе, Goroutine има малка намеса на OS kernel, което прави превключването на контекста (context switching) на потребителско ниво (User-Level) по-евтино от единица OS thread, позволявайки бързо превключване на задачите.
Освен това, използва M:N модел за управление, като се разпределя към OS thread-ове. Чрез създаване на OS thread pool, обработката е възможна с малко на брой нишки, без да са необходими много. Например, ако изпадне в състояние на изчакване като system call, Go runtime изпълнява друга горутина в OS thread-а, като по този начин OS thread-ът не бездейства и ефективно използва CPU, което позволява бърза обработка.
Благодарение на това Golang може да постигне по-висока производителност от другите езици, особено при I/O операции.
Лаконична (concise)
Голямо предимство е и възможността лесно да се обработва функция само с един ключова дума go
, когато е необходима едновременност.
Необходимо е използването на сложни заключвания като Mutex
, Semaphore
и неизбежно трябва да се вземе предвид състоянието на DeadLock, което е задължително при използване на заключвания, което налага сложни етапи още от фазата на проектиране преди разработката.
Goroutine, следвайки философията "Не комуникирай, като споделяш памет, а споделяй памет, като комуникираш", препоръчва предаване на данни чрез Channel
и поддържа функцията SELECT
, която в комбинация с Channel позволява обработката да започне от канала, в който данните са готови. Освен това, използването на sync.WaitGroup
позволява лесно изчакване на приключването на всички горутини, което улеснява управлението на работния поток. Благодарение на тези инструменти се предотвратяват проблеми с конкуренцията на данни между нишките и се осигурява по-безопасна обработка на едновременността.
Също така, чрез използването на context, може да се контролира жизненият цикъл, отмяната, изчакването (timeout), крайният срок (deadline) и обхватът на заявката на потребителско ниво (User-Level), което гарантира определено ниво на сигурност.
Паралелна работа на Goroutine (GOMAXPROCS)
Говорихме за предимствата на едновременността на goroutine, но може да се запитате дали не поддържа паралелизъм. Това е така, защото броят на ядрата на съвременните CPU надхвърля двуцифрено число, за разлика от миналото, и дори домашните компютри имат значителен брой ядра.
Въпреки това, Goroutine извършва и паралелна работа, което се осъществява чрез GOMAXPROCS
.
Ако GOMAXPROCS
не е настроен, той се задава по различен начин в зависимост от версията.
Преди 1.5: Основна стойност 1, ако е необходимо повече от 1, задължително е настройването по начин като
runtime.GOMAXPOCS(runtime.NumCPU())
.1.5 ~ 1.24: Променена е на всички налични логически ядра. От този момент нататък не е необходимо разработчикът да прави настройки, освен ако не е наложително.
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 Scheduler (M:N модел)
Връщайки се към предишната точка, че се управлява чрез разпределяне към OS thread-ове, използвайки M:N модел, по-конкретно съществува моделът goroutine GMP.
- G (Goroutine): Най-малката работна единица, изпълнявана в Go
- M (Machine): OS thread (действително място на изпълнение)
- P (Processor): Логически процес, управляван от Go runtime
P допълнително притежава локална опашка за изпълнение (Local Run Queue) и действа като scheduler, който разпределя присвоените G към M. Накратко, goroutine е
Процесът на работа на GMP е както следва:
- Когато G (Goroutine) се създаде, той се присвоява към локалната опашка за изпълнение на P (Processor).
- P (Processor) присвоява G (Goroutine) от локалната опашка за изпълнение към M (Machine).
- M (Machine) връща състоянието на G (Goroutine): block, complete, preempted.
- Work-Stealing (Кражба на работа): Ако локалната опашка за изпълнение на P се изпразни, друго P проверява глобалната опашка. Ако и там няма G (Goroutine), то краде работа от друго локално P (Processor), за да гарантира, че всички M работят без прекъсване.
- Обработка на System Call (Blocking): Ако по време на изпълнение на G (Goroutine) възникне Block, M (Machine) преминава в състояние на изчакване. В този момент P (Processor) се отделя от блокирания M (Machine) и се свързва с друг M (Machine), за да изпълни следващия G (Goroutine). По този начин няма загуба на CPU дори по време на изчакване при I/O операция.
- Ако един G (Goroutine) е зает (preempted) за дълго време, се дава възможност за изпълнение на друг G (Goroutine).
GC (Garbage Collector) на Golang също се изпълнява върху Goroutine, което позволява паралелно почистване на паметта с минимално прекъсване на изпълнението на приложението (STW), като по този начин се използват ефективно системните ресурси.
И накрая, Golang е едно от силните предимства на езика, а има и много други, така че се надявам много разработчици да се насладят на Go.
Благодаря.