Goroutine-perusteet
Goroutine
Kun pyydetään gophereita kertomaan golangin eduista, usein esiin nousee samanaikaisuuteen (Concurrency) liittyvä kirjoitus. Tämän sisällön perustana on goroutine, joka on kevyt ja yksinkertaisesti käsiteltävissä. Olen laatinut siitä lyhyen kuvauksen.
Samanaikaisuus (Concurrency) vs. rinnakkaisuus (Parallelism)
Ennen kuin ymmärrämme goroutinen, haluan ensin käsitellä kahta usein sekoitettua käsitettä.
- Samanaikaisuus: Samanaikaisuus liittyy monien tehtävien käsittelyyn samanaikaisesti. Se ei välttämättä tarkoita, että ne suoritetaan todella samanaikaisesti, vaan se on rakenteellinen, looginen käsite, jossa useat tehtävät jaetaan pieniin yksiköihin ja suoritetaan vuorotellen, jolloin käyttäjälle näyttää siltä, että useita tehtäviä käsitellään samanaikaisesti. Samanaikaisuus on mahdollista myös yhden ytimen järjestelmissä.
- Rinnakkaisuus: Rinnakkaisuus on "useiden tehtävien samanaikainen käsittely useilla ytimillä". Se on nimensä mukaisesti tehtävien rinnakkaista suorittamista, ja se suorittaa eri tehtäviä samanaikaisesti.
Goroutine mahdollistaa samanaikaisuuden helpon toteutuksen Go-ajonaikaisen ajoittajan (runtime scheduler) avulla, ja se hyödyntää luonnollisesti myös rinnakkaisuutta GOMAXPROCS
-asetuksen kautta.
Yleisesti käytetty Javan Multi thread, jolla on korkea käyttöaste, on tyypillinen rinnakkaisuuden käsite.
Miksi goroutine on hyvä?
Kevyt (lightweight)
Sen luomiskustannukset ovat erittäin alhaiset verrattuna muihin kieliin. Tästä herää kysymys, miksi golang käyttää vähän resursseja? Syynä on se, että luontipaikkaa hallitaan Go-ajonaikaisen ympäristön sisällä. Tämä johtuu siitä, että se on edellä mainittu kevyt looginen säie, joka on pienempi kuin OS-säieyksikkö, ja sen alkupino vaatii noin 2 KB:n koon ja on dynaamisesti muuttuva pinon lisäyksellä käyttäjän toteutuksen mukaan.
Koska sitä hallitaan pinoyksiköissä, luominen ja poistaminen on erittäin nopeaa ja edullista, mikä mahdollistaa miljoonien goroutinejen käsittelyn ilman suurta kuormitusta. Tämän ansiosta Goroutine voi minimoida OS-ytimen puuttumisen asiaan ajonaikaisen ajoittajan ansiosta.
Suorituskykyinen (performance)
Ensinnäkin, kuten edellä selitettiin, Goroutine vaatii vähemmän OS-ytimen puuttumista, joten kontekstin vaihto käyttäjätasolla (User-Level) on edullisempaa kuin OS-säieyksiköllä, mikä mahdollistaa nopean tehtävien vaihdon.
Lisäksi se hallinnoi OS-säikeitä M:N-mallin avulla. OS-säiepoolin luominen mahdollistaa käsittelyn pienemmällä määrällä säikeitä ilman monien säikeiden tarvetta. Esimerkiksi, jos järjestelmäkutsu (system call) aiheuttaa odotustilan, Go-ajonaikainen ympäristö suorittaa toisen goroutinen OS-säikeessä, jolloin OS-säie ei lepää, vaan hyödyntää CPU:ta tehokkaasti, mikä mahdollistaa nopean käsittelyn.
Tämän ansiosta Golang pystyy saavuttamaan korkean suorituskyvyn erityisesti I/O-toiminnoissa verrattuna muihin kieliin.
Ytimekäs (concise)
Se on suuri etu, että samanaikaisuutta vaativat funktiot voidaan käsitellä helposti yhdellä go
-avainsanalla.
On käytettävä monimutkaisia lukkoja, kuten Mutex
ja Semaphore
, ja lukkojen käytön yhteydessä on välttämättä otettava huomioon DeadLock-tila, mikä tekee suunnitteluvaiheesta ennen kehitystä monimutkaisen.
Goroutine suosittelee tiedonvälitystä Channel
-mekanismin kautta noudattaen filosofiaa "Älä kommunikoi jakamalla muistia, vaan jaa muistia kommunikoimalla", ja SELECT
yhdessä Channel
-mekanismin kanssa tukee toimintoa, joka mahdollistaa käsittelyn siitä kanavasta, jossa data on valmis. Lisäksi sync.WaitGroup
:n avulla voidaan yksinkertaisesti odottaa, kunnes kaikki goroutinet ovat päättyneet, mikä helpottaa työskentelykulun hallintaa. Näiden työkalujen ansiosta voidaan estää säikeiden välinen datakilpailu ja käsitellä samanaikaisuutta turvallisemmin.
Lisäksi kontekstin (context) avulla voidaan käyttäjätasolla (User-Level) hallita elinkaarta, peruuttamista, aikakatkaisua, määräaikaa ja pyyntöjen laajuutta, mikä takaa tietynasteisen vakauden.
Goroutinen rinnakkaistyöskentely (GOMAXPROCS)
Vaikka goroutinen samanaikaisuus on hyvä asia, saatatte miettiä, tukeeko se rinnakkaisuutta. Nykyään CPU-ytimien määrä ylittää kaksi numeroa, toisin kuin menneisyydessä, ja myös kotitietokoneissa on huomattava määrä ytimiä.
Goroutine kuitenkin suorittaa myös rinnakkaistyöskentelyä, ja se on GOMAXPROCS
.
Jos GOMAXPROCS
ei ole asetettu, se asetetaan eri tavoin versiosta riippuen.
Ennen 1.5: Oletusarvo 1, asetus on pakollinen, jos tarvitaan enemmän kuin 1, esimerkiksi
runtime.GOMAXPOCS(runtime.NumCPU())
.1.5 – 1.24: Muutettiin käyttämään kaikkia käytettävissä olevia loogisia ytimiä. Tästä lähtien kehittäjän ei tarvitse asettaa sitä, ellei ole erityistä rajoitustarvetta.
1.25: Kielenä, joka on tunnettu konttiympäristöissä, se tarkistaa linuxin cGroupin ja selvittää konttiin asetetun
CPU-rajoituksen
.Tällöin, jos loogisia ytimiä on 10 ja CPU-rajoitusarvo on 5,
GOMAXPROCS
asetetaan pienempään arvoon, eli 5.
Versio 1.25:n muutos on erittäin merkittävä. Tämä johtuu siitä, että kielen käyttöaste konttiympäristöissä on noussut. Tämän ansiosta voidaan estää tarpeettomien säikeiden luominen ja kontekstin vaihto, mikä vähentää CPU:n kuristumista (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: alkaa
14 time.Sleep(10 * time.Millisecond) // Viive työn simulointia varten
15 fmt.Printf("Goroutine %d: 시작\n", name) // Goroutine %d: alkaa
16}
17
18func main() {
19 runtime.GOMAXPROCS(2) // Käytä vain 2 CPU-ydintä
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이 끝날 때까지 대기합니다...") // Odotetaan, kunnes kaikki goroutinet ovat valmiit...
29 wg.Wait()
30 fmt.Println("모든 작업이 완료되었습니다.") // Kaikki tehtävät on suoritettu.
31
32}
33
Goroutinen ajoittaja (M:N-malli)
Edellisessä osiossa, jossa käsiteltiin goroutinen hallintaa M:N-mallin avulla OS-säikeisiin allokoimalla, on vielä tarkempi malli, nimittäin goroutine GMP-malli.
- G (Goroutine): Pienin Go:ssa suoritettava työskentely-yksikkö
- M (Machine): OS-säie (todellinen työskentelypaikka)
- P (Processor): Go-ajonaikaisen ympäristön hallinnoima looginen prosessori
P:llä on lisäksi paikallinen suoritusjono (Local Run Queue), ja se toimii ajoittajana, joka allokoi allokoidut G:t M:lle. Yksinkertaisesti goroutinen
GMP:n toimintaprosessi on seuraava:
- Kun G (Goroutine) luodaan, se allokoidaan P:n (Processor) paikalliseen suoritusjonoon.
- P (Processor) allokoi paikallisessa suoritusjonossa olevan G:n (Goroutine) M:lle (Machine).
- M (Machine) palauttaa G:n (Goroutine) tilan: block, complete, preempted.
- Work-Stealing (työn varastaminen): Jos P:n paikallinen suoritusjono tyhjenee, toinen P tarkistaa globaalin jonon. Jos sielläkään ei ole G:tä (Goroutine), se varastaa työn toiselta paikalliselta P:ltä (Processsor) varmistaakseen, että kaikki M:t toimivat keskeytyksettä.
- Järjestelmäkutsun käsittely (Blocking): Jos G (Goroutine) joutuu Block-tilaan suorituksen aikana, M (Machine) joutuu odotustilaan. Tällöin P (Processor) irrottaa Block-tilaan joutuneen M:n (Machine) ja yhdistää sen toiseen M:ään (Machine) seuraavan G:n (Goroutine) suorittamiseksi. Tällöin CPU:ta ei tuhlata I/O-työskentelyn odotusajankaan aikana.
- Jos yksi G (Goroutine) varaa (preempted) resurssin pitkäksi aikaa, se antaa suoritusmahdollisuuden toiselle G:lle (Goroutine).
Golangin GC (Garbage Collector) suoritetaan myös Goroutinen päällä, mikä mahdollistaa muistin rinnakkaisen puhdistamisen minimoiden sovelluksen suorituksen keskeytykset (STW) ja käyttää järjestelmäresursseja tehokkaasti.
Lopuksi, Golang on yksi kielen vahvoista eduista, ja niitä on monia muitakin, joten toivon, että monet kehittäjät nauttivat Golangista.
Kiitos.