GoSuda

Основи на Go Routines

By hamori
views ...

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. Преди 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 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 е както следва:

  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. Обработка на System Call (Blocking): Ако по време на изпълнение на G (Goroutine) възникне Block, 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.

Благодаря.