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) }
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("*") }
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) }
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) }
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) }
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) }
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… 😉
Commenti
Nelle varie printer, non si vede il “<-c".
Grazie, correggo. Nell’anteprima era tutto OK 😦
Trackback
[…] OK, continuo l’esame della concorrenza (concurrency) in Go, iniziata nel post precedente, questo. […]
[…] 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 […]