Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Le goroutine

Le goroutine del linguaggio GO permettono di parallelizzare l'esecuzione dei thread in modo da evitare rallentamenti
Le goroutine del linguaggio GO permettono di parallelizzare l'esecuzione dei thread in modo da evitare rallentamenti
Link copiato negli appunti

Il linguaggio Go nasce per affrontare ogni tipo di situazione fronteggiando database, connessioni in rete, gestione di dati su file e tanto altro. Tutte queste attività hanno in comune una cosa: tempi di latenza spesso rilevanti.

Se dobbiamo salvare molti dati su un file o scaricarli dalla Rete dovremo fare i conti con la possibilità che tali operazioni richiedano tempo per essere concluse e che la congestione di hardware e connessioni possa portare anche ad un loro prolungamento.

Pertanto, è necessario fare in modo che tali attività, per così dire, vengano svolte su una sorta di "corsia parallela".

Thread e attività asincrone

Senza pretendere di trattare in questa sede temi complessi sui sistemi operativi, richiamiamo alcuni concetti tanto per contestualizzare il discorso.

Quando avviamo un programma (ad esempio, quello che abbiamo realizzato in GO), attiviamo un processo nel sistema operativo. Questo al suo interno ha una serie di "corsie" su cui le operazioni vengono svolte, dette thread. I programmi generalmente hanno inizialmente solo un thread principale, pertanto se svolgessimo operazioni lente su di esso tutta l'esecuzione ne risulterebbe rallentata.

Per questo ogni volta che dobbiamo gestire accessi in rete, lettura e scritture di dati o altre operazioni soggette potenzialmente a latenze rilevanti dovremmo aprire un nuovo thread, una nuova "corsia" diciamo, su cui farle svolgere in modo tale che il flusso principale del programma possa proseguire senza rallentamenti.

Quando utilizziamo dei thread secondari a questo scopo diciamo che le operazioni su di essi distaccate saranno eseguite in maniera asincrona: in parole povere, nessuno rimarrà immobile ad aspettare il loro risultato. Tutto ciò può essere fatto in Go in modo assolutamente agevole con costrutti facili da implementare ed efficienti da eseguire detti goroutine.

Implementare una goroutine

Per implementare una goroutine dovremo seguire solo due passaggi:

  • scrivere una funzione che raccoglie attività "lente";
  • chiamare la funzione del punto precedente anteponendo la parola chiave go.

Nient'altro. Mettiamo subito mano al codice, ci dilungheremo in seguito con qualche considerazione. Senza scomodare alcuna reale attività impegnativa svolgiamo quello che può essere considerato l'esempio didattico per eccellenza in questa materia: sfruttiamo Sleep del modulo time per produrre artificiosamente un ritardo di qualche secondo adoperandoci con un po' di fantasia per immaginare che sia prodotto da una query onerosa su database o dallo scaricamento di molti dati.

Scriviamo un programmino senza goroutine in cui viene chiamata la funzione esecuzione_lenta e in cui svilupperemo un ritardo di 5 secondi:

package main
import (
    "fmt"
    "time"
)
func esecuzione_lenta() {
    fmt.Println("esecuzione_lenta inizia")
    time.Sleep(time.Second*5)
    fmt.Println("esecuzione_lenta ha finito")
}
func main() {
    fmt.Println("INIZIO...")
    fmt.Println("Chiamo la funzione esecuzione_lenta")
    esecuzione_lenta()
    fmt.Println("FINE")
}

Lo script si chiama esempio_goroutine.go e lo eseguiamo con go run esempio_goroutine.go. L'output produce le seguenti stringhe:

INIZIO...
Chiamo la funzione esecuzione_lenta
esecuzione_lenta inizia
esecuzione_lenta ha finito
FINE

Tra l'apparizione di "esecuzione_lenta inizia" e di "esecuzione_lenta ha finito" passeranno alcuni secondi e solo dopo l'uscita dalla funzione potremo vedere apparire il messaggio "FINE". Questo perché senza usare goroutine, sia esecuzione_lenta sia il main saranno eseguiti sullo stesso thread, quindi per concludersi il programma dovrà aspettare che il flusso di esecuzione esca da esecuzione_lenta.

Per eseguire quest'ultima con goroutine - quindi, in maniera asincrona - ci basterà una piccolissima modifica, ovvero chiamare la funzione nel main con la parola
chiave go dinnanzi, proprio così:

package main
import (
    "fmt"
    "time"
)
func esecuzione_lenta() {
    fmt.Println("esecuzione_lenta inizia")
    time.Sleep(time.Second*5)
    fmt.Println("esecuzione_lenta ha finito")
}
func main() {
    fmt.Println("INIZIO...")
    fmt.Println("Chiamo la funzione esecuzione_lenta")
    go esecuzione_lenta()
    fmt.Println("FINE")
}

Così facendo otterremo il seguente output che non conterrà nemmeno le stringhe prodotte dalla funzione esecuzione_lenta:

INIZIO...
Chiamo la funzione esecuzione_lenta
FINE

Il motivo di ciò è che, ora, essendo la funzione eseguita come goroutine su un thread secondario, il main non "aspetta" più la sua fine ma prosegue sul suo thread principale facendo addirittura finire il programma prima della conclusione di esecuzione_lenta: ciò dimostra che la funzione è stata eseguita in maniera asincrona.

Qualora eseguendolo capitasse di vedere apparire in output "esecuzione_lenta inizia" non significherà niente di diverso da quanto appena detto: la funzione eseguita andrà parallelamente in concorrenza con il main e potrebbe fare in tempo ad emettere il proprio output prima della fine del thread principale.

Di sicuro, non faremo in tempo a vedere "esecuzione_lenta ha finito" in quanto il thread principale, senza più rallentamenti, proseguirà spedito come un treno.

Considerazioni

Di questo esempio dobbiamo cogliere il significato centrale, ovvero che siamo riusciti a "parallelizzare" l'esecuzione su più thread. Nella realtà, come si immagina, il tutto prenderà connotati leggermente diversi:

  • al posto di un ritardo intenzionale vi saranno davvero operazioni da svolgere separatamente per non rallentare il thread principale;
  • il main non abbandonerà così le goroutine senza aspettarle ma continuerà a gestire il loro lavoro, portando avanti l'esecuzione del programma che però, a differenza di una esecuzione mono-thread, non subirà rallentamenti sia prevedibili sia (peggio ancora) imprevisti, causati da congestione delle vie di comunicazione.

Altro aspetto interessante: qualora non volessimo definire una funzione apposita, una goroutine può essere lanciata al volo utilizzando una sorta di funzione anonima. L'esempio di prima potrebbe infatti essere rielaborato così:

package main
import (
    "fmt"
    "time"
)
func main() {
    fmt.Println("INIZIO...")
    go func(){
        fmt.Println("esecuzione_lenta inizia")
        time.Sleep(time.Second*5)
        fmt.Println("esecuzione_lenta ha finito")
    }()
    fmt.Println("FINE")
}

Un'invocazione di questo tipo può essere adottata per una serie di motivi tra cui la brevità del codice da eseguire, che non comprometterebbe la leggibilità del blocco che la contiene, e la necessità di non creare una funzione apposita che sarebbe comunque richiamata solo in questo caso.

Concludendo, le goroutine sono facili da implementare, portano grandi vantaggi e sono leggere a livello di esecuzione. Hanno anche loro dei limiti però, in particolare, sono pensate per essere eseguite in parallelo (anche in gran numero contemporaneamente) ma non dispongono di mezzi per comunicare tra loro. Anche a questo però, come vedremo, il linguaggio Go ha saputo porre un ottimo rimedio: i channel.


Ti consigliamo anche