Gorutinat, kanalet, sinkronizimi

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
All Rights Reserved Theme by 404 THEME.