Grunderna för Go-routines
Goroutine
När gophers ombeds att beskriva fördelarna med Golang, är Concurrency ett frekvent ämne. Grunden för detta är den lätta och enkla hanteringen av goroutine. Jag har kortfattat skrivit om detta.
Concurrency vs Parallelism
Innan vi förstår goroutine, vill jag först ta upp två begrepp som ofta förväxlas.
- Concurrency: Concurrency handlar om att hantera många uppgifter samtidigt. Det betyder inte nödvändigtvis att de körs samtidigt i verkligheten, utan det är ett strukturellt och logiskt koncept där flera uppgifter delas upp i mindre enheter och körs omväxlande, vilket får det att framstå för användaren som om flera uppgifter behandlas samtidigt. Concurrency är möjligt även med en enda kärna.
- Parallelism: Parallelism innebär att "flera uppgifter behandlas samtidigt på flera kärnor". Det är, som namnet antyder, en parallell utförande av uppgifter, där olika uppgifter körs samtidigt.
Goroutine möjliggör enkel implementering av concurrency genom Go runtime scheduler, och den utnyttjar naturligt parallelism genom GOMAXPROCS
-inställningen.
Java:s Multi-thread, som ofta har hög användning, är ett typiskt exempel på parallelism.
Varför är Goroutine bra?
Lättviktig
Kostnaden för att skapa goroutines är mycket låg jämfört med andra språk. Frågan uppstår varför Golang använder så få. Svaret är att skapandet hanteras internt av Go runtime. Detta beror på att det är en lättviktig logisk tråd; den är mindre än en OS-tråd, kräver en initial stackstorlek på cirka 2 KB och kan dynamiskt variera genom att lägga till stackar beroende på användarens implementering.
Eftersom den hanteras i stackenheter, är skapande och borttagning mycket snabbt och billigt, vilket gör att även miljontals goroutines kan hanteras utan problem. Som ett resultat kan Goroutine minimera OS-kärnans inblandning tack vare runtime scheduler.
Hög prestanda
För det första, som nämnts ovan, involverar Goroutine minimal OS-kärninteraktion, vilket gör att kontextväxling på User-Level är billigare än på OS-trådnivå, vilket möjliggör snabba uppgiftsbyten.
Dessutom används en M:N-modell för att tilldela och hantera OS-trådar. Genom att skapa en OS-trådpool kan bearbetning ske med färre trådar än vad som annars skulle behövas. Om till exempel ett systemanrop leder till ett vänteläge, kan Go runtime exekvera en annan goroutine på OS-tråden, vilket gör att OS-tråden inte vilar och effektivt utnyttjar CPU:n för snabb bearbetning.
Detta resulterar i att Golang kan uppnå högre prestanda, särskilt vid I/O-operationer, jämfört med andra språk.
Koncis
En stor fördel är också att en funktion enkelt kan hanteras med nyckelordet go
när concurrency behövs.
Att använda komplexa Lock-mekanismer som Mutex
och Semaphore
är nödvändigt, och om Lock används måste man oundvikligen överväga deadlocks, vilket kräver komplexa steg redan i designfasen före utvecklingen.
Goroutine rekommenderar dataöverföring via Channel
i enlighet med filosofin "Don't communicate by sharing memory; share memory by communicating". SELECT
stöder även en funktion som, i kombination med Channel
, gör det möjligt att behandla data från den kanal som är redo först. Dessutom, med sync.WaitGroup
, kan man enkelt vänta tills alla goroutines är klara, vilket förenklar hanteringen av arbetsflödet. Tack vare dessa verktyg kan datakollisionsproblem mellan trådar undvikas och concurrency kan hanteras säkrare.
Dessutom kan livscykel, avbrytning, timeout, deadline och begäransomfång kontrolleras på User-Level med hjälp av context
, vilket säkerställer en viss grad av stabilitet.
Goroutine:s parallella uppgifter (GOMAXPROCS)
Även om concurrency är en fördel med goroutine, kanske du undrar om den inte stöder parallelism. Nuförtiden är antalet kärnor i CPU:er mer än tvåsiffrigt, och även hushållsdatorer har ett betydande antal kärnor.
Men Goroutine utför även parallella uppgifter, och det är GOMAXPROCS
.
Om GOMAXPROCS
inte är inställt, konfigureras det olika beroende på version:
Före 1.5: Standardvärde 1. Om mer än 1 behövs, är det obligatoriskt att ställa in det med metoder som
runtime.GOMAXPOCS(runtime.NumCPU())
.1.5 till 1.24: Ändrades till antalet tillgängliga logiska kärnor. Från och med nu behöver utvecklare inte ställa in det om det inte finns ett starkt behov.
1.25: Som ett språk som är känt i container-miljöer, kontrollerar det cGroup i Linux för att bestämma
CPU-begränsningen
som är inställd för containern.Om antalet logiska kärnor är 10 och CPU-begränsningen är 5, kommer
GOMAXPROCS
att ställas in på det lägre värdet, det vill säga 5.
Ändringen i 1.25 är en mycket betydande förbättring. Detta beror på att språkets användbarhet i container-miljöer har ökat. Till följd av detta har onödig trådgenerering och kontextväxling minskat, vilket förhindrar 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: Startar\n", name) // Startar Goroutine
14 time.Sleep(10 * time.Millisecond) // Fördröjning för arbetssimulering
15 fmt.Printf("Goroutine %d: Slutar\n", name) // Slutar Goroutine
16}
17
18func main() {
19 runtime.GOMAXPROCS(2) // Använd endast 2 CPU-kärnor
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("Väntar på att alla goroutines ska avslutas...")
29 wg.Wait()
30 fmt.Println("Alla uppgifter är slutförda.")
31
32}
33
Goroutine:s Schemaläggare (M:N-modell)
I den föregående delen, där jag nämnde att M:N-modellen används för att tilldela och hantera OS-trådar, kommer vi nu att gå in mer i detalj på goroutine GMP-modellen.
- G (Goroutine): Den minsta arbetsenheten som exekveras i Go.
- M (Machine): OS-tråd (den faktiska arbetsplatsen).
- P (Processor): En logisk process som hanteras av Go runtime.
P har dessutom en Local Run Queue och fungerar som en schemaläggare som tilldelar tilldelade G till M. Enkelt uttryckt, goroutine är
GMP:s funktionsprincip är som följer:
- När en G (Goroutine) skapas, tilldelas den till P:s (Processor) Local Run Queue.
- P (Processor) tilldelar G (Goroutine) från Local Run Queue till M (Machine).
- M (Machine) returnerar G:s (Goroutine) status: block, complete, preempted.
- Work-Stealing: Om P:s Local Run Queue blir tom, kontrollerar andra P:n den globala kön. Om det inte finns några G (Goroutines) där heller, stjäl de arbete från andra lokala P (Processorer) för att se till att alla M är ständigt aktiva.
- System Call Handling (Blocking): Om en G (Goroutine) blockeras under exekvering, övergår M (Machine) till ett vänteläge. I detta fall kopplas P (Processor) bort från den blockerade M (Machine) och kopplas till en annan M (Machine) för att exekvera nästa G (Goroutine). Under denna process slösas ingen CPU-tid under I/O-operationers väntetider.
- Om en G (Goroutine) innehar processorn under en längre tid (preempted), ges exekveringstillfället till en annan G (Goroutine).
Golang:s GC (Garbage Collector) körs också ovanpå Goroutine, vilket möjliggör parallell minneshantering med minimal avbrott (STW) av applikationens exekvering, vilket effektivt använder systemresurser.
Slutligen är Golang en av språkets starka fördelar, och det finns många fler. Jag hoppas att många utvecklare kommer att njuta av Golang.
Tack.