Goroutine-perusteet
Goroutine
Kun gophereita pyydetään kertomaan golangin eduista, usein esille nousee samanaikaisuus (Concurrency). Tämän perusta on kevyt ja yksinkertaisesti käsiteltävä goroutine. Olen kirjoittanut tästä lyhyesti.
Samanaikaisuus (Concurrency) vs. rinnakkaisuus (Parallelism)
Ennen kuin ymmärrämme goroutinea, haluan käydä läpi kaksi usein sekoitettua käsitettä.
- Samanaikaisuus: Samanaikaisuus tarkoittaa monien tehtävien käsittelyä samanaikaisesti. Se ei välttämättä tarkoita, että ne suoritetaan oikeasti samanaikaisesti, vaan se on rakenteellinen ja looginen käsite, jossa useita tehtäviä jaetaan pienempiin yksiköihin ja suoritetaan vuorotellen, jolloin käyttäjän näkökulmasta näyttää siltä, että useita tehtäviä käsitellään samanaikaisesti. Samanaikaisuus on mahdollista myös yksisydämisessä järjestelmässä.
- Rinnakkaisuus: Rinnakkaisuus tarkoittaa "monia tehtäviä, jotka käsitellään samanaikaisesti useilla ytimillä". Kuten nimi antaa ymmärtää, tehtävät etenevät rinnakkain, ja eri tehtäviä suoritetaan samanaikaisesti.
Goroutine helpottaa samanaikaisuuden toteuttamista Go-ajonaikaisen ajoittimen avulla, ja se hyödyntää luonnollisesti myös rinnakkaisuutta GOMAXPROCS
-asetuksen kautta.
Yleisesti käytetty Javan multithreading on tyypillinen esimerkki rinnakkaisuudesta.
Miksi goroutine on hyvä?
Kevyt (lightweight)
Sen luontikustannukset ovat erittäin alhaiset verrattuna muihin kieliin. Herää kysymys, miksi golang käyttää vähemmän resursseja? Tämä johtuu siitä, että luonti hallitaan Go-ajonaikaisen ympäristön sisällä. Tämä johtuu siitä, että kyseessä on edellä mainittu kevyt looginen säie, joka on pienempi kuin käyttöjärjestelmän säieyksikkö, ja alkuperäinen pinokoko vaatii noin 2 kt tilaa ja voi muuttua dynaamisesti pinon lisäämisen myötä käyttäjän toteutuksen mukaan.
Koska sitä hallitaan pinoyksiköittäin, luonti ja poisto ovat erittäin nopeita ja edullisia, mikä mahdollistaa miljoonien goroutinejen käsittelyn ilman merkittävää kuormitusta. Tämän ansiosta Goroutine voi minimoida käyttöjärjestelmäytimen puuttumisen ajonaikaisen ajoittimen ansiosta.
Hyvä suorituskyky (performance)
Ensinnäkin, kuten edellä selitettiin, Goroutine vaatii vähemmän käyttöjärjestelmäytimen puuttumista, joten kontekstin vaihto käyttäjätasolla on edullisempaa kuin käyttöjärjestelmän säikeiden tasolla, mikä mahdollistaa tehtävien nopean vaihtamisen.
Lisäksi se hallitaan allokoimalla se käyttöjärjestelmän säikeisiin M:N-mallia käyttäen. Luomalla käyttöjärjestelmän säiepoolin se voi käsitellä tehtäviä pienemmällä määrällä säikeitä ilman, että tarvitaan monia säikeitä. Esimerkiksi, jos järjestelmäkutsu joutuu odotustilaan, Go-ajonaikainen ympäristö suorittaa toisen goroutinen käyttöjärjestelmän säikeessä, jolloin käyttöjärjestelmän säie ei ole toimettomana ja hyödyntää tehokkaasti CPU:ta nopeaan käsittelyyn.
Tämän ansiosta Golang voi saavuttaa korkean suorituskyvyn erityisesti I/O-tehtävissä verrattuna muihin kieliin.
Ytimekäs (concise)
Suuri etu on myös se, että kun samanaikaisuutta tarvitaan, funktio voidaan käsitellä helposti yhdellä go
-avainsanalla.
On käytettävä monimutkaisia lukkoja, kuten Mutex
ja Semaphore
, ja lukkojen käytön yhteydessä on pakko ottaa huomioon deadlocking-tilat, mikä tekee kehitystä edeltävästä suunnitteluvaiheesta monimutkaisen.
Goroutine suosittelee tietojen siirtämistä Channel
-kanavien kautta filosofian mukaan "älä kommunikoi jakamalla muistia, vaan jaa muistia kommunikoimalla", ja SELECT
yhdistettynä kanaviin (Channel) tukee jopa ominaisuutta, joka mahdollistaa tietojen käsittelyn kanavasta, jossa tiedot ovat valmiina. Lisäksi sync.WaitGroup
:in avulla voidaan yksinkertaisesti odottaa, kunnes kaikki goroutinet ovat valmiita, mikä helpottaa työnkulun hallintaa. Näiden työkalujen ansiosta voidaan estää säikeiden välisten tietojen kilpailuongelmia ja käsitellä samanaikaisuutta turvallisemmin.
Lisäksi kontekstin (context) avulla voidaan hallita elinkaarta, peruuttamista, aikakatkaisuja, määräaikoja ja pyyntöalueita käyttäjätasolla (User-Level), mikä takaa tietynasteisen vakauden.
Goroutinen rinnakkaiset tehtävät (GOMAXPROCS)
Vaikka goroutinen samanaikaisuus on hyvä asia, saatat miettiä, tukeeko se rinnakkaisuutta. Nykyisten CPU-ytimien määrä on yli kaksi numeroa, toisin kuin aiemmin, ja jopa kotitietokoneissa on huomattava määrä ytimiä.
Mutta Goroutine suorittaa myös rinnakkaisia tehtäviä, ja se on GOMAXPROCS
.
Jos GOMAXPROCS
-asetusta ei ole määritetty, se asetetaan eri tavoin versiosta riippuen.
Ennen versiota 1.5: Oletusarvo 1, jos tarvitaan yli 1, on pakollista asettaa esimerkiksi
runtime.GOMAXPOCS(runtime.NumCPU())
.1.5 ~ 1.24: Muutettu kaikkien käytettävissä olevien loogisten ytimien määrään. Tästä lähtien kehittäjän ei tarvitse asettaa tätä, ellei ole erityisen rajoittavia tarpeita.
1.25: Kuten konttiympäristöissä tunnetulle kielelle sopii, se tarkistaa Linuxin cGroupin ja selvittää konttiin asetetun
CPU-rajoituksen
.Jos loogisia ytimiä on 10 ja CPU-rajoitus on 5,
GOMAXPROCS
asetetaan pienempään arvoon 5.
Versio 1.25:n muutos on erittäin merkittävä. Se johtuu siitä, että kielen käyttökelpoisuus konttiympäristöissä on kasvanut. Tämän seurauksena tarpeettomien säikeiden luominen ja kontekstinvaihdot ovat vähentyneet, mikä estää CPU-kuristuksen (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: alkaa\n", name) // Goroutine %d: alkaa
14 time.Sleep(10 * time.Millisecond) // Viive tehtävän simulointia varten
15 fmt.Printf("Goroutine %d: valmis\n", name) // Goroutine %d: valmis
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("Odotetaan kaikkien goroutinejen päättymistä...") // Odotetaan kaikkien goroutinejen päättymistä...
29 wg.Wait()
30 fmt.Println("Kaikki tehtävät on suoritettu.") // Kaikki tehtävät on suoritettu.
31
32}
33
Goroutinen ajoitin (M:N-malli)
Edellisen osan "hallitaan allokoimalla se käyttöjärjestelmän säikeisiin M:N-mallia käyttäen" tarkempi kuvaus on goroutine GMP-malli.
- G (Goroutine): Go:n pienin suoritettava työyksikkö
- M (Machine): Käyttöjärjestelmän säie (todellinen työpaikka)
- P (Processor): Go-ajonaikaisen ympäristön hallitsema looginen prosessi
P:llä on lisäksi paikallinen suoritusjono (Local Run Queue) ja se toimii ajoittimena, joka allokoi G:n M:lle. Yksinkertaisesti sanottuna goroutine on
GMP:n toimintaperiaate on seuraava:
- Kun G (Goroutine) luodaan, se allokoidaan P:n (Processor) paikalliseen suoritusjonoon (Local Run Queue).
- 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 (Local Run Queue) tyhjenee, toinen P tarkistaa globaalin jonon. Jos sieltäkään ei löydy G:tä (Goroutine), se varastaa työn toiselta paikalliselta P:ltä (Processor) varmistaakseen, että kaikki M:t ovat jatkuvasti toiminnassa.
- Järjestelmäkutsujen käsittely (Blocking): Jos G (Goroutine) joutuu Block-tilaan suorituksen aikana, M (Machine) siirtyy odotustilaan. Tällöin P (Processor) irrottautuu Block-tilassa olevasta M:stä (Machine) ja yhdistyy toiseen M:ään (Machine) suorittaakseen seuraavan G:n (Goroutine). Tällöin CPU-resursseja ei tuhlata I/O-tehtävien odotusaikana.
- Jos yksi G (Goroutine) valtaa resurssit pitkäksi aikaa (preempted), muille G:ille (Goroutine) annetaan mahdollisuus suorittaa.
Golangin roskankerääjä (GC) suoritetaan myös Goroutinen päällä, mikä minimoi sovelluksen suorituksen keskeytykset (STW) ja mahdollistaa muistin rinnakkaisen puhdistuksen, hyödyntäen järjestelmäresursseja tehokkaasti.
Lopuksi, Golang on yksi kielen vahvoista eduista, ja niitä on monia muitakin, joten toivon, että monet kehittäjät nauttivat Go:sta.
Kiitos.