Go – Concorrenza I

10

I programmi di grosse dimensioni sono spesso costituiti da più sottoprogrammi più piccoli. Come nel caso di un web server che gestisce le richieste come sottoprogrammi.
L’ideale per un programma è la capacità di eseguire questi componenti nello stesso tempo (per il web erver gestire le varie richieste). Lo svolgere simultaneamente più di un compito simultaneamente è conosciuto come concorrenza (concurrency) e Go è speciale per questo con goroutines e channels.

Goroutines

Una goroutine è una funzione che è capace di girare contemporaneamente con altre funzioni. Per creare una goroutine si usa la keyword go seguita dall’invocazione:

package main

    import "fmt"

func f(n int) {
    for i := 0; i < 10; i++ {
        fmt.Print(n, ":", i, "|| ")
    }
}

func main() {
    go f(0)
    var input string
    fmt.Scanln(&input)
}

g0
Qui abbiamo 2 goroutines, il main e f(). Normalmente quando si chiama una funzione questa viene interamente eseguita prima di tornare all’istruzione immediatamente successiva a quella che l’ha chiamata. Con le goroutines si torna immediatamente alla linea successiva, senza attendere il completamento della funzione. Nel caso in esame il ‘puter è troppo veloce per riuscire a interrompere la f() se parte.
Però se tolgo la Scanln() succede questo:

package main

    import "fmt"

func f(n int) {
    for i := 0; i < 10; i++ {
        fmt.Print(n, ":", i, "|| ")
    }
}

func main() {
    go f(0)
    fmt.Println("*")
}

g1
E, sì! torna subito e finisce senza completare lo f().

Le goroutines sono leggere e se ne possono creare agevolmente migliaia. Ecco come si può modificare il programma precedente per eseguire contemporaneamente 10 goroutines:

package main

    import "fmt"

func f(n int) {
    for i := 0; i < 10; i++ {
        fmt.Print(n, ":", i, "|| ")
    }
}

func main() {
    for i := 0; i < 10; i++ {
        go f(i)
    }
    var input string
    fmt.Scanln(&input)
}

g2
OK, già si vede che vengono eseguite contemporaneamente; per evidenziare questo funzionamento possiamo inserire delle pause con time.Sleep() in modo random con rand.Intn()

package main

import (
    "fmt"
    "time"
    "math/rand"
)

func f(n int) {
    for i := 0; i < 10; i++ {
        fmt.Print(n, ":", i, "|| ")
        amt := time.Duration(rand.Intn(250))
        time.Sleep(time.Millisecond * amt)
    }
}

func main() {
    for i := 0; i < 10; i++ {
        go f(i)
    }
    var input string
    fmt.Scanln(&input)
}

g3
Ok, quasi simile al precedente; il mio ‘puter non è velocissimissimo, ce ne sono di più veloci ma questo è il mio ‘puter 😉

Channels (canali?)

Dubbio: posso chiamarli canali? o si fa confusione? mah!?
I canali forniscono un modo per permettere a due goroutines di comunicare fra loro e sincronizzarsi, esempio:

package main

import (
    "fmt"
    "time"
)

func pinger(c chan string) {
for i := 0; ; i++ {
    c <- "ping"
    }
}

func printer(c chan string) {
    for {
        msg :=  <- c
        fmt.Println(msg)
        time.Sleep(time.Second * 1)
    }
}

func main() {
    var c chan string = make(chan string)

    go pinger(c)
    go printer(c)

    var input string
    fmt.Scanln(&input)
}

g4
Se non batti invio continua, un ping al secondo, nèh!

Il canale è rappresentato dalla keyword chan seguita dal tipo delle cose passate, in questo caso passiamo stringhe. L’operatore < (freccia a sinistra, letf arrow) è usato per inviare e ricevere messaggi: c <- "ping" significa invia “ping”, e msg := <- c significa ricevi in messaggio e memorizzalo in msg.
La linea di fmt può essere scritta anche così fmt.Println(<-c) eliminando la variabile msg.

Usando un canale come questo sincronizziamo le due goroutines. Quando pinger() tenta di inviare un messaggio sul canale attende finché printer() è pronta a ricevere il messaggio (questo è conosciuto come bloccaggio, blocking). Aggiungendo un altra goroutine che invia messaggi (ponger()) vediamo cosa succede:

package main

import (
    "fmt"
    "time"
)

func pinger(c chan string) {
for i := 0; ; i++ {
    c <- "ping"
    }
}

func ponger(c chan string) {
    for i := 0; ; i++ {
        c <- "pong"
    }
}

func printer(c chan string) {
    for {
        fmt.Println(<- c)
        time.Sleep(time.Second * 1)
    }
}

func main() {
    var c chan string = make(chan string)

    go pinger(c)
    go ponger(c)
    go printer(c)

    var input string
    fmt.Scanln(&input)
}

g5
OK, abbiamo il ping-pong.

Direzione del canale (channel direction)

Si può specificare la direzione su un canale, restringendolo a solo inviare o ricevere. Con

func pinger(c chan<- string)

c può solo inviare; tentando di ricevere genera un errore di compilazione. Similmente si può avere

func printer(c <-chan string)

che può solo ricevere. Regola pratica (mia) guardare se <- va in chan o ne esce).

Un canale che non ha queste restrizioni è conosciuto come bidirezionale e può essere passato a funzioni che prendono canali con restrizioni ma non viceversa (logicamente).

OK, troppo lungo, continua, prossimamente… 😉

Posta un commento o usa questo indirizzo per il trackback.

Commenti

  • dantasm  Il 5 febbraio 2013 alle 13:03

    Nelle varie printer, non si vede il “<-c".

    • juhan  Il 5 febbraio 2013 alle 13:06

      Grazie, correggo. Nell’anteprima era tutto OK 😦

Trackback

  • Go – Concorrenza II « Ok, panico su 14 febbraio 2013 alle 10:55

    […] OK, continuo l’esame della concorrenza (concurrency) in Go, iniziata nel post precedente, questo. […]

  • Giocando a ping pong in Go | Ok, panico su 28 Maggio 2013 alle 09:51

    […] subito il codice in Go (tra l’altro già presentato anche da Juhan in una variante): nella funzione main() vengono lanciate due goroutine che eseguono […]

Lascia un commento

Questo sito utilizza Akismet per ridurre lo spam. Scopri come vengono elaborati i dati derivati dai commenti.