Основи на Go Routines
Горутина
Ако помолите гофъри да разкажат за предимствата на golang, често ще срещнете статии, свързани с едновременност (Concurrency). Основата на това съдържание е горутината (goroutine), която е лека и лесна за обработка. Накратко написах за нея.
Едновременност (Concurrency) срещу Паралелизъм (Parallelism)
Преди да разберем горутините, искам първо да изясня две често бъркани концепции.
- Едновременност: Едновременността е свързана с обработката на много задачи едновременно. Това не означава непременно, че те се изпълняват едновременно в действителност, а е структурна и логическа концепция, при която множество задачи се разделят на по-малки единици и се изпълняват последователно, така че за потребителя изглежда, че много задачи се обработват едновременно. Едновременността е възможна дори и на едноядрен процесор.
- Паралелизъм: Паралелизмът означава "едновременна обработка на множество задачи на множество ядра". Буквално, това е паралелно изпълнение на задачи, като различни задачи се изпълняват едновременно.
Горутините лесно позволяват реализирането на едновременност чрез Go runtime scheduler и естествено използват паралелизъм чрез настройката GOMAXPROCS
.
Multi thread на Java, която често се използва, е типична концепция за паралелизъм.
Защо горутините са добри?
Леки (lightweight)
Разходите за създаване са много ниски в сравнение с други езици. Тук възниква въпросът защо golang използва малко ресурси. Това е така, защото мястото за създаване се управлява вътрешно от Go runtime. Причината е, че това са леки логически нишки, по-малки от единици OS нишки, изискващи начален стек от около 2KB, и динамично променливи чрез добавяне на стек в зависимост от потребителската имплементация.
Управлението на стека позволява много бързо и евтино създаване и изтриване, което прави възможно обработката на милиони горутини без натоварване. В резултат на това Goroutine може да сведе до минимум намесата на OS ядрото благодарение на runtime scheduler.
Производителност (performance)
Първо, Goroutine, както е обяснено по-горе, има малка намеса на OS ядрото, така че превключването на контекста на потребителско ниво (User-Level) е по-евтино от единиците OS нишки, което позволява бързо превключване на задачите.
Освен това, той използва M:N модел за присвояване и управление на OS нишки. Чрез създаване на OS thread pool, могат да се обработват задачи с по-малко нишки, без да е необходимо много нишки. Например, ако се стигне до състояние на изчакване като системен извикване, Go runtime изпълнява друга горутина на OS нишката, така че OS нишката не бездейства и ефективно използва CPU, което позволява бърза обработка.
В резултат на това Golang може да постигне по-висока производителност от други езици, особено при I/O операции.
Кратък (concise)
Голямо предимство е и лесното обработване на функции с единствена ключова дума go
, когато е необходима едновременност.
Необходимо е да се използват сложни Lock механизми като Mutex
, Semaphore
и при използване на Lock е задължително да се вземе предвид състоянието на DeadLock, което изисква сложни етапи още от фазата на проектиране преди разработката.
Goroutine препоръчва предаване на данни чрез Channel
съгласно философията "не споделяйте памет чрез комуникация, а комуникирайте, за да споделяте памет", а SELECT
поддържа функция, която позволява обработка от канала, който е готов за данни, в комбинация с Channel. Освен това, използването на sync.WaitGroup
позволява лесно изчакване, докато всички горутини приключат, улеснявайки управлението на работния поток. Благодарение на тези инструменти може да се предотврати проблемът с конкуренцията на данни между нишките и да се осигури по-безопасна едновременна обработка.
Освен това, използвайки контекст (context), може да се контролира жизнения цикъл, анулирането, изтичането на времето, крайния срок и обхвата на заявката на потребителско ниво (User-Level), което осигурява известна степен на стабилност.
Паралелни задачи на Goroutine (GOMAXPROCS)
Говорихме за предимствата на едновременността на горутините, но сигурно се чудите дали не поддържа паралелизъм. Броят на ядрата на съвременните процесори надхвърля двуцифрено число, за разлика от миналото, а домашните компютри също имат немалък брой ядра.
Но 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) // Отпечатва съобщение за стартиране на горутината.
14 time.Sleep(10 * time.Millisecond) // Забавяне за симулиране на работа.
15 fmt.Printf("Goroutine %d: 시작\n", name) // Отпечатва съобщение за стартиране на горутината.
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이 끝날 때까지 대기합니다...") // Отпечатва съобщение, че се чака всички горутини да приключат.
29 wg.Wait() // Изчаква всички горутини да приключат.
30 fmt.Println("모든 작업이 완료되었습니다.") // Отпечатва съобщение, че всички задачи са завършени.
31
32}
33
Планировчик на Goroutine (M:N модел)
В предишния раздел, където се обсъжда M:N моделът за присвояване и управление на OS нишки, ще разгледаме по-конкретно GMP модела на goroutine.
- G (Goroutine): Най-малката единица работа, изпълнявана в Go.
- M (Machine): OS нишка (действително място на изпълнение).
- P (Processor): Логически процесор, управляван от Go runtime.
P допълнително притежава локална опашка за изпълнение (Local Run Queue) и действа като планировчик, който назначава присвоените 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 работят без прекъсване.
- Обработка на системни извиквания (Blocking): Ако G (Goroutine) бъде блокирана по време на изпълнение, M (Machine) преминава в състояние на изчакване. В този момент P (Processor) се отделя от блокираната M (Machine) и се свързва с друга M (Machine), за да изпълни следващата G (Goroutine). По този начин няма загуба на CPU дори по време на изчакване на I/O операция.
- Ако една G (Goroutine) превземе (preempted) за дълго време, тя дава възможност за изпълнение на друга G (Goroutine).
Golang също така изпълнява GC (Garbage Collector) над Goroutine, което позволява паралелно почистване на паметта с минимално прекъсване на изпълнението на приложението (STW), като по този начин ефективно използва системните ресурси.
И накрая, Golang е едно от силните предимства на езика, а има и много други, така че се надявам много разработчици да се насладят на Go.
Благодаря.