Konkurrenca
Ndër karakteristikat më të rëndësishme të Go është se ka përkrahje pët konkurrencë (concurrency). Me konkurrencë nënkuptojmë ekzekutimin e njëkohshëm të disa funksioneve, që në Go quhen gorutina (goroutines). Konkurrenca nuk nënkupton paralelizëm, por procese të ndara ku secili proces ka një detyrë të caktuar dhe mund të komunikojnë ndërmjet vete nëpërmes kanaleve.
Nëse procesori posedon vetëm një bërthamë, atëherë programi do të ekzekutohet në mënyrë sekuencionale, ku një gorutinë ekzekutohet pas gorutinës tjetër.
Suporti për konkurrencë në Go nëse rastet kur procesori ka më shumë bërthama (cores) mundëson shfrytëzimin e të gjitha bërthamave të procesorit për ekzekutimin sa më efiçent të një numri të madh të gorutinave.
Gorutinat
Gorutinat janë funksione apo metoda që ekzekutohen në mënyrë konkurrent me funksionet apo metodat tjera. Gorutinat mund të imagjinohen si threads me peshë më të vogël ( light weight threads). Kostoja e krijimit të një gorutine (në kuptim të resurseve procesorike) është më e ultë se e një thread. Prandaj, tipike për aplikacionet në Go është ekzekutimi konkurrent i mijëra gorutinave.
Gorutina, pra, është një detyrë e cila ekzekutohet në mënyrë të pavarur. Go mundëson edhe koordinimin ndërmjet gorutinave të ndryshme.
Gorutinat kundrejt threads
Gorutinat janë shumë më pak të kushtueshme në krahasim me threads. Ato zënë vetëm disa kilobajtë në stack dhe stack-u mund të rritet e zvogëlohet në varësi prej nevojave të aplikacionit, gjersa kur kemi të bëjmë me threads, madhësia e stack-ut duhet të specifikohet dhe të jetë fikse.
Gorutinat shfrytëzojnë threads, ku një thread-i i korrespondojnë shumë gorutina, në raste edhe me mijëra. Nëse për shembull një thread është i bllokuar në pritje të inputit nga ana e përdoruesit, atëherë ajo gorutinë bartet në një thread tjetër i cili krijohet aty për aty, prej nga vazhdon ekzekutimi i gorutinës.
Për gjithë procesin e menaxhimit të gorutinave dhe threads përkujdeset runtime, prandaj në kodin e aplikacionit nuk kemi nevojë të implementojmë kurrfarë logjike në lidhje me gorutinat.
Gorutinat komunikojnë duke përdorur kanalet. Kanalet mundësojnë parandalimin e ndodhjes së race conditions
në rastet kur bëhet qasje në memorjen e ndarë (shared memory) nga gorutinat.
Kur startohet një gorutinë, thirrja e atij funksioni kthen (return) menjëherë. Për dallim prej funksioneve, kontrollli nuk do të presë që gorutina ta përfundojë ekzekutimin. Kontrolli kalon menjëherë në rreshtin vijues të kodit pas thirrjes së gorutinës dhe çfarëdo vlere kthyese nga gorutina do të injorohet.
Edhe funksioni main()
\është gorutinë, dhe ai duhet të jetë duke u ekzekutuar për t’i mundësuar ekzekutimin gorutinave të tjera. Nëse main ndërpret ekzekutimin, atëherë programi do ta ndërpresë ekzekutimin dhe asnjë gorutinë tjetër nuk do të ekzekutohet.
Ta marrim shembullin e thirrjes së një funksioni nga një cikël, pra brenda një iteracioni:
package main
import "fmt"
func main() {
for x := 1; x <= 10; x++ {
printo(x)
}
}
func printo(i int) {
fmt.Println(i)
}
https://play.golang.org/p/EIAeMTefQIc
Rezultati:
1
2
3
4
5
6
7
8
9
10
Në këtë program, 10 herë përsëritet thirrja e funksionit printo()
, i cili e shfaq në ekran vlerën e inkrementit. Të gjitha thirrjet e funksionit bëhen në mënyrë sekuencionale, d.m.th. pa e kryer ekzekutimin i funksionit brenda një cikli nuk bëhet thirrje e sërishme e atij funksioni.
Tash ta bëjmë të njëjtën por duke përdorur gorutinat:
package main
import (
"fmt"
"time"
)
func main() {
for x := 1; x <= 10; x++ {
go printo(x)
}
time.Sleep(100 * time.Millisecond)
}
func printo(i int) {
fmt.Println(i)
}
https://play.golang.org/p/VNAXEkGGg3N
Rezultati:
1
2
5
8
3
10
7
9
4
6
Për ta ekzekutuar një funksion në formë gorutine, duhet që para thirrjes së funksionit të shënohet go
, pra: go printo(x)
. Me këtë do të krijohen 10 gorutina, ku secila ekzekutohet si thread
në vete, ku ndonjë gorutinë ekzekutohet e pavarur nga një gorutinë tjetër, e ndonjë gorutinë ekzekutohet në formë sekuencionale, krejt në varësi prej numrit të bërthamave të procesorit. I tërë ky proces menaxhohet nga runtime i Go.
Vërejmë se renditja e rezultateve nuk është në sekuencë të njëjtë rritëse nga 1 deri 10, siç ishte rasti me shembullin e mëparshëm. Kjo ndodh për shkak se secila gorutinë është thread
në vete, ku ndonjë gorutinë ekzekutohet më heret, ndonjë më vonë, dhe rezultatet shfaqen pikërisht sipas renditjes së kryerjes së ekzekutimit të gorutinave.
Pauza në fund: time.Sleep(100 * time.Millisecond)
është e domosdoshme, në mënyrë që funksioni main() të arrijë t’i pranojë rezultatet e gorutinave. Nëse e fshijmë atë rresht dhe e startojmë programin, do të shohim se nuk do të shfaqet asnjë rezultat sepse funksioni main() do të jetë i ekzekutuar ende pa arritur të kthehen rezultatet nga gorutinat.
Në secilin ekzekutim vijues të programit, renditja e rezultateve mund të ndryshojë. Këtë shembull duhet ta kompajlojmë dhe ekzekutojmë lokalisht, sepse në Go Playground mund të ekzekutohet vetëm një thread
prandaj gjithmonë do të gjenerohet sekuenca e njëjtë prej 1 deri 10, sepse të gjitha gorutinat ekzekutohen në mënyrë sekuencionale njëra pas tjetrës.
Pauzën në fund të funksionit main()
mund ta realizojmë edhe në mënyra të tjera, për shembull duke e përdorur funksionin fmt.Scanln()
që do të presë derisa përdoruesi ta shtyp tastin Enter.
Goroutinat mund t’i thërrasim edhe nga një closure.
Fillimisht, marrim shembullin e një closure që thirret nga një for iteracion, i cili e thirr një funksion por jo si gorutinë.
package main
import (
"fmt"
)
func main() {
numrat := []int{10, 25, 40}
for _, i := range numrat {
func() {
numero(i)
}()
}
}
func numero(from int) {
for i := from; i <= from+10; i += 5 {
fmt.Println(i)
}
}
https://play.golang.org/p/H1lp-LSCVHW
Rezultati:
10
15
20
25
30
35
40
45
50
Shohim se është formuar një seri sekuencionale e numrave nga 10 deri në 50, me hap 5.
Tash e provojmë të njëjtën, tash duke e thirrur closure me go
.
package main
import (
"fmt"
)
func main() {
numrat := []int{10, 25, 40}
for _, i := range numrat {
func() {
go numero(i)
}()
}
fmt.Scanln()
}
func numero(from int) {
for i := from; i <= from + 10; i += 5 {
fmt.Println(i)
}
}
https://play.golang.org/p/q2kmPapJeqI
Rezultati:
10
15
20
40
25
45
50
30
35
Tash vërejmë se vlerat nuk janë sekuencionale dhe kjo ndodh për shkak të vetë natyrës së gorutinave ku secila gorutinë është proces në vete dhe nuk e kemi të garantuar me çfarë radhitje do të kthehen rezultatet e secilës gorutinë veç e veç.
Në situata të këtilla, kur një closure thirret nga një cikël, mund të ndodhë që në momentin kur closure e lexo vlerën e iteratorit, ajo vlerë mos të jetë vlera e radhës, kështu që kurrë me saktësi nuk mund ta dijmë se cilën vlerë të iteratorit do ta marrë closure. Pra, mund të ndodhë që për ndonjë vlerë mos të startohet një gorutinë.
Këtë problem e zgjidhim duke e bartur vlerën e iteratorit në closure duke e dhënë si parametër:
package main
import (
"fmt"
)
func main() {
numrat := []int{10, 25, 40}
for _, i := range numrat {
func(x int) {
go numero(x)
}(i)
}
fmt.Scanln()
}
func numero(from int) {
for i := from; i <= from+10; i += 5 {
fmt.Println(i)
}
}
https://play.golang.org/p/ygHdYAPS8CH
Rezultati:
25
30
35
40
45
50
10
15
20
Shembull nga https://golangbot.com/goroutines/
package main
import (
"fmt"
"time"
)
func numbers() {
for i := 1; i <= 5; i++ {
time.Sleep(250 * time.Millisecond)
fmt.Printf("%d ", i)
}
}
func alphabets() {
for i := 'a'; i <= 'e'; i++ {
time.Sleep(400 * time.Millisecond)
fmt.Printf("%c ", i)
}
}
func main() {
go numbers()
go alphabets()
time.Sleep(3000 * time.Millisecond)
fmt.Println("main terminated")
}
Rezultati:
1 a 2 3 b 4 c 5 d e main terminated
Sinkronizimi
Go posedon me një pako të quajtur sync që mundëson thjeshtimin e procesit të sinkronizimit të goritunave.
Në situata të thjeshta, gorutinat mund të sinkronizohen nëpërmes kanaleve.
Pritja e një gorutine të vetme
Channels
Pritja e një gorutine të vetme mund të implementohet nëpërmes përdorimit të një kanali. Kur e përfundon ekzekutimin, gorutina dërgon mesazh gorutinës kryesore e cila është në pritje.
Shembull me funksion anonim:
package main
import "fmt"
func main() {
msg := make(chan string)
fmt.Println("main(): Thirret gorutina")
go func() {
fmt.Println("Gorutina u thirr")
msg <- "Gorutina perfundoi"
}()
m := <-msg
fmt.Println(m)
}
https://play.golang.org/p/BaDRWB7Lqcv
Rezultati:
main(): Thirret gorutina
Gorutina u thirr
Gorutina perfundoi
Shembull me funksion:
package main
import (
"fmt"
"time"
)
func worker(fund chan bool) {
fmt.Println("Gorutina filloi")
time.Sleep(time.Second)
fmt.Println("Gorutina mbaroi")
fund <- true
}
func main() {
fund := make(chan bool)
fmt.Println("main(): gorutina do të startohet")
go worker(fund)
fmt.Println("main(): duke pritur gorutinën të përfundojë")
<-fund
fmt.Println("main(): U krye")
}
https://play.golang.org/p/Z6Dlva11tLm
Rezultati:
main(): gorutina do të startohet
main(): duke pritur gorutinën të përfundojë
Gorutina filloi
Gorutina mbaroi
main(): U krye
Channels buffering
Në mënyrën standarde, kanalet janë unbuffered
, që do të thotë se një kanal mund të pranojë dërgesë (chan <-) vetëm nëse ekziston pranuesi (<- chan) për ta pranuar vlerën e dërguar.
Kanalet që janë buffered
mund të pranojnë një numër të specifikuar të vlerave pa pranues korrespondues për këto vlera. Në shembullin vijues krijohet buffer
me 2 vlera.
package main
import "fmt"
func main() {
m := make(chan string, 2)
m <- "mesazhi 1"
m <- "mesazhi 2"
fmt.Println(<-m)
fmt.Println(<-m)
}
https://play.golang.org/p/7xjLQxVGQsz
Rezultati:
mesazhi 1
mesazhi 2
Drejtimi i kanalit
Kur përdoren kanalet si parametra të funksionit, mund të specifikojmë nëse kanali duhet vetëm të dergojë apo vetëm të pranojë vlera.
Nëse një kanal e deklarojmë vetëm si pranues, kompajleri do të raportojë gabim gjatë kompajlimit nëse brenda kodit e përdorim si dërgues.
package main
import "fmt"
func main() {
dergimi := make(chan string, 1)
pranimi := make(chan string, 1)
dergo(dergimi, "Mesazhi u përcoll")
prano(dergimi, pranimi)
fmt.Println(<-pranimi)
}
func dergo(dergimi chan<- string, mesazhi string) {
dergimi <- mesazhi
}
func prano(dergimi <-chan string, pranimi chan<- string) {
mesazhi := <-dergimi
pranimi <- mesazhi
}
https://play.golang.org/p/rOvR1KQeIa6
Rezultati:
Mesazhi u përcoll
Select
Go’s select lets you wait on multiple channel operations. Combining goroutines and channels with select is a powerful feature of Go.
Select mundëson pritjen e operacioneve të shumëfishta të kanalit. Kombinimi i gorutinave dhe kanaleve me select
është karakteristikë e fuqishme e Go.
package main
import (
"fmt"
"time"
)
func main() {
k1 := make(chan string)
k2 := make(chan string)
go gorutina1(k1)
go gorutina2(k2)
for i := 0; i < 2; i++ {
select {
case mesazhi1 := <-k1:
fmt.Println("U pranua: ", mesazhi1)
case mesazhi2 := <-k2:
fmt.Println("U pranua: ", mesazhi2)
}
}
}
func gorutina1(k1 chan<- string) {
time.Sleep(1 * time.Second)
k1 <- "i pari"
}
func gorutina2(k2 chan<- string) {
time.Sleep(2 * time.Second)
k2 <- "i dyti"
}
https://play.golang.org/p/agbUdEqqzZk
Rezultati:
U pranua: i pari
U pranua: i dyti
Timeouts
Timeouts janë të rëndësishme për programet që konektohen në resurset eksterne, ku nuk e kanë të garantuar se resursi ekstern do të jetë i qasshëm brenda një intervali të paracaktuar.
package main
import (
"fmt"
"time"
)
func main() {
k1 := make(chan string, 1)
go func() {
time.Sleep(2 * time.Second)
k1 <- "Unë jam gorutina 1"
}()
select {
case rezultati := <-k1:
fmt.Println(rezultati)
case <-time.After(1 * time.Second):
fmt.Println("Koha kaloi për gorutinën 1")
}
k2 := make(chan string, 1)
go func() {
time.Sleep(2 * time.Second)
k2 <- "Unë jam gorutina 2"
}()
select {
case res := <-k2:
fmt.Println(res)
case <-time.After(3 * time.Second):
fmt.Println("Koha kaloi për gorutinën 2")
}
}
https://play.golang.org/p/7y-rXJYSnZk
Rezultati:
Koha kaloi për gorutinën 1
Unë jam gorutina 2
Non-Blocking Channel Operations
Mbyllja e kanaleve
Range over Channels
Timers
Tickers
Worker Pools
WaitGroups
Rate Limiting
Atomic Counters
Mutexes
Stateful Goroutines
Sinkronizimi i kanaleve
Në shembullin vijues do të demonstrohet sinkrinozimi i gorutinës nëpërmes kanalit. Në këtë rast do të përdoret kanali me emrin perfundoi
nëpërmes të cilit gorutina do t’ia përcjellë funksionit main()
informatën se një proces ka përfunduar. Me rreshtin perfundoi <- true
shkaktohet pritje në funksionin main()
derisa të pranohet mesazhi nga gorutina.
package main
import (
"fmt"
"time"
)
func main() {
perfundoi := make(chan bool, 1)
go funksioni(perfundoi)
<-perfundoi
}
func funksioni(perfundoi chan bool) {
fmt.Print("Duke punuar...")
time.Sleep(time.Second)
fmt.Println("U krye")
perfundoi <- true
}
https://play.golang.org/p/MKSUA5NVmbq
Rezultati:
Duke punuar...U krye
Pritja e më shumë gorutinave
sync
Nëse nevojitet të presim përfundimin e gorutinave të shumëfishta, do ta përdorim pakon sync.WaitGroup
. Gorutina main e thërret metodën Add
për të specifikuar numrin e gorutinave që duhen pritur. Më pastaj, çdo gorutinë ekzekutohet dhe thërret metodën Done
kur ta përfundojnë ekzekutimin. Wait
mund të përdoret për bllokim gjersa të përfundojnë të gjitha gorutinat.
package main
import (
"fmt"
"sync"
"time"
)
func worker(wg *sync.WaitGroup, id int) {
defer wg.Done()
fmt.Printf("Gorutina %v: filloi\n", id)
time.Sleep(time.Second)
fmt.Printf("Gorutina %v: përfundoi\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
fmt.Println("main(): Duke e startuar gorutinën", i)
wg.Add(1)
go worker(&wg, i)
}
fmt.Println("main(): Duke pritur gorutinat të përfundojnë")
wg.Wait()
fmt.Println("main(): Përfundoi")
}
https://play.golang.org/p/p7StS5oK6nX
Rezultati:
main(): Duke e startuar gorutinën 1
main(): Duke e startuar gorutinën 2
main(): Duke e startuar gorutinën 3
main(): Duke pritur gorutinat të përfundojnë
Gorutina 3: filloi
Gorutina 1: filloi
Gorutina 2: filloi
Gorutina 1: përfundoi
Gorutina 3: përfundoi
Gorutina 2: përfundoi
main(): Përfundoi