Il supporto per i programmi simultanei (concurrent programming) in Go è un aspetto nuovo e molto interessante. Vediamo un esempio semplice e pratico
CONCURRENT, GOROUTINE E CHANNELS
Una caratteristica peculiare e distintiva di Go è il supporto nativo per la scrittura di programmi concurrent, con goroutine e channels.
APPLICAZIONE SEQUENZIALE
In un programma classico sequenziale, il tempo impiegato per l’esecuzione totale del processo è pari alla somma di tutte le funzioni che l’applicazione dovrĂ eseguire uno in seguito all’altra.
Per capire meglio la differenza che intercorre tra un programma lineare e uno concurrent riscriviamo, come esempio, un programma simile a curl
: inserendo un indirizzo URL stampa a video il testo della pagina web. Logicamente il codice può essere ampliato con l’aggiunta di numerose altre opzioni, ma è volutamente basico.
CODICE
package main
import (
"fmt"
"io/ioutil"
"net/http"
"os"
)
func main() {
for _, url := range os.Args[1:] { // Lettura URL che inseriamo nella riga di comando
risp, err := http.Get(url) // Esecuzione richiesta
if err != nil { // Controllo se la richiesta ha restituito un errore
fmt.Fprintf(os.Stderr, "Errore richiesta URL: %v\n", err) // Stampa l'errore
os.Exit(1) // Uscita dal programma
}
b, err := ioutil.ReadAll(risp.Body) // Lettura contenuto della pagina web
risp.Body.Close()
if err != nil { // Controllo se la lettura della pagina ha restituito un errore
fmt.Fprintf(os.Stderr, "Errore in lettura %s: %v\n", url, err) // Stampa l'errore
os.Exit(1) // Uscita dal programma
}
fmt.Printf("%s", b) //Stampa il contenuto della pagina
}
}
FUNZIONAMENTO
Il funzionamento di questo programma è semplice: la funzione http.Get
esegue la richiesta HTTP e, se non ci sono errori, ritorna la risposta nella struttura risp
. Nel campo Body
di risp
è contenuta la risposta in un formato tale da poter essere letto e interpretato. Successivamente, ioutil.Readall
legge la risposta per intero e la salva nella variabile b
. Lo stream di Body
è chiuso per evitare di utilizzare troppe risorse inutilmente e, per finire, viene stampato a video con Printf
la risposta in un output standard.
|
|
Mentre se la richiesta HTTP fallisce, otterremmo questo risultato:
|
|
In entrambi i casi di errore, os.Exit(1)
farĂ chiudere il programma con un codice di errore pari a 1.
CODICE PER FUNZIONI SIMULTANEE
Il prossimo codice che propongo, recupera simultaneamente degli URLs in modo tale che il processo impiegherĂ come tempo massimo la risposta piĂą lunga degli URLs.
Sono omessi intenzionalmente controlli vari per rendere il codice piĂą leggibile.
CODICE
package main
import (
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"time"
)
func main() {
inizio := time.Now()
ch := make(chan string)
for _, url := range os.Args[1:] {
go funzioneRecupero(url, ch) // inizio di a goroutine
}
for range os.Args[1:] {
fmt.Println(<-ch) // ricevo da channel ch
}
fmt.Printf("%.2fs passato\n", time.Since(inizio).Seconds())
}
func funzioneRecupero(url string, ch chan<- string) {
inizio := time.Now()
resp, err := http.Get(url)
if err != nil {
ch <- fmt.Sprint(err) // invio a channel ch
return
}
nbytes, err := io.Copy(ioutil.Discard, resp.Body)
resp.Body.Close() // liberiamo le risorse
if err != nil {
ch <- fmt.Sprintf("mentre leggo %s: %v", url, err)
return
}
secs := time.Since(inizio).Seconds()
ch <- fmt.Sprintf("%.2fs %7d %s", secondi, nbytes, url)
}
L’output sarĂ il seguente
|
|
FUNZIONAMENTO
Una goroutine
è una funzione eseguita simultaneamente. Un channel
è un meccanismo di comunicazione che permette a una goroutine il passaggio di un valore specifico a un’altra goroutine. la funzione main è eseguita in una goroutine e la parola go crea ulteriori goroutines.
La funzione main crea un channel di testo usando make
. Per ogni argomento della command-line, l’istruzione go
nel primo loop
inizia una nuova goroutine che richiama funzioneRecupero in modo asincrono per recuperare l’URL attraverso http.Get
. La funzione io.Copy
legge il contenuto della risposta inviandolo a ioutil.Discard
. Copy
ritorna sia il conteggio dei byte sia qualsiasi errore, se è presente. Per ogni risultato, la funzione funzioneRecupero invia i dati a ch
, il channel. Il secondo loop in main lo riceve e stampa i dati.
Quando una goroutine tenta di inviare o ricevere su un channel, si blocca fino a quando un’altra goroutine tenta una operazione di ricezione o invio e in quel determinato momento il valore è trasferito ed entrambe le goroutine continuano il loro lavoro. Nel secondo esempio, ogni funzioneRecupero invia un valore (ch <- espressione) al channel ch
, e main le riceve tutte (<- ch). In main il comando di print assicura che l’output di ogni goroutine è processata singolarmente, evitando problemi se due goroutines finiscono nello stesso istante.