Ogni linguaggio di programmazione di successo ha qualche caratteristica killer che ha reso il successo. Il punto di forza di Go è la programmazione concorrente. È stato progettato attorno a un solido modello teorico (CSP) e fornisce sintassi a livello di linguaggio nella forma della parola chiave "go" che avvia un'attività asincrona (sì, la lingua prende il nome dalla parola chiave) e un modo incorporato per comunicare tra attività concorrenti.
In questo articolo (prima parte), introdurrò il modello CSP che implementa la concorrenza di Go, le goroutine e come sincronizzare l'operazione di più goroutine di cooperazione. In un prossimo articolo (seconda parte), scriverò sui canali di Go e su come coordinare tra le goroutine senza strutture di dati sincronizzate.
CSP sta per Communicating Sequential Processes. Fu introdotto per la prima volta da Tony (C. A. R.) Hoare nel 1978. CSP è un framework di alto livello per la descrizione di sistemi concorrenti. È molto più semplice programmare programmi concorrenti corretti quando si opera a livello di astrazione CSP rispetto al tipico livello di astrazione di thread e blocchi.
Le goroutine sono un gioco sulle coroutine. Tuttavia, non sono esattamente gli stessi. Una goroutine è una funzione che viene eseguita su un thread separato dal thread di avvio, quindi non lo blocca. Più goroutine possono condividere lo stesso thread del sistema operativo. A differenza delle coroutine, le goroutine non possono esplicitamente cedere il controllo a un'altra goroutine. Il runtime di Go si occupa di trasferire implicitamente il controllo quando una particolare goroutine bloccherebbe l'accesso I / O.
Vediamo un po 'di codice. Il programma Vai sotto definisce una funzione, chiamata in modo creativo "f", che dorme in modo casuale fino a mezzo secondo e quindi stampa il suo argomento. Il principale()
la funzione chiama il f ()
funzione in un ciclo di quattro iterazioni, dove in ogni iterazione chiama f ()
tre volte con "1", "2" e "3" di fila. Come ci si aspetterebbe, l'output è:
--- Esegui in sequenza come normali funzioni 1 2 3 1 2 3 1 2 3 1 2 3
Quindi invoca principale f ()
come una goroutine in un ciclo simile. Ora i risultati sono diversi perché il runtime di Go eseguirà il f
goroutine contemporaneamente, e quindi poiché il sonno casuale è diverso tra le goroutine, la stampa dei valori non avviene nell'ordine f ()
è stato invocato. Ecco l'output:
--- Esegui simultaneamente come goroutine 2 2 3 1 3 2 1 3 1 1 3 2 2 1 3
Il programma stesso usa i pacchetti di librerie standard "time" e "math / rand" per implementare la sospensione casuale e l'attesa alla fine di tutte le goroutine da completare. Questo è importante perché quando il thread principale termina, il programma è terminato, anche se ci sono ancora goreutine in sospeso.
pacchetto main import ("fmt" "time" "math / rand") var r = rand.New (rand.NewSource (time.Now (). UnixNano ())) func f (s stringa) // Sleep up to ritardo di mezzo secondo: = time.Duration (r.Int ()% 500) * time.Millisecond time.Sleep (delay) fmt.Println (s) func main () fmt.Println ("--- Esegui in sequenza come funzioni normali ") per i: = 0; io < 4; i++ f("1") f("2") f("3") fmt.Println("--- Run concurrently as goroutines") for i := 0; i < 5; i++ go f("1") go f("2") go f("3") // Wait for 6 more seconds to let all go routine finish time.Sleep(time.Duration(6) * time.Second) fmt.Println("--- Done.")
Quando hai un sacco di goroutine selvagge che corrono dappertutto, spesso vuoi sapere quando sono finiti.
Ci sono diversi modi per farlo, ma uno degli approcci migliori è usare a WaitGroup
. UN WaitGroup
è un tipo definito nel pacchetto "sync" che fornisce il Inserisci()
, Fatto()
e Aspettare()
operazioni. Funziona come un contatore che conta quante routine di go sono ancora attive e aspetta fino a quando non sono state completate. Ogni volta che inizi una nuova goroutine, chiami Aggiungere (1)
(puoi aggiungerne più di uno se avvii più routine di go). Quando una goroutine è terminata, chiama Fatto()
, che riduce il conteggio di uno, e Aspettare()
blocchi finché il conteggio non raggiunge lo zero.
Convertiamo il programma precedente per usare a WaitGroup
invece di dormire per sei secondi, nel caso in cui alla fine. Si noti che il f ()
funzione utilizza differire wg.Done ()
invece di chiamare wg.Done ()
direttamente. Questo è utile per garantire wg.Done ()
viene sempre chiamato, anche se c'è un problema e la goroutine termina presto. Altrimenti, il conteggio non raggiungerà mai lo zero e wg.Wait ()
può bloccare per sempre.
Un altro piccolo trucco è che io chiamo wg.Add (3)
solo una volta prima di invocare f ()
tre volte. Nota che chiamo wg.Add ()
anche quando invocando f ()
come una funzione regolare. Questo è necessario perché f ()
chiamate wg.Done ()
indipendentemente dal fatto che funzioni come una funzione o goroutine.
pacchetto main import ("fmt" "time" "math / rand" "sync") var r = rand.New (rand.NewSource (time.Now (). UnixNano ())) var wg sync.WaitGroup func f (s string) defer wg.Done () // Sospendi fino a mezzo secondo ritardo: = time.Duration (r.Int ()% 500) * time.Millisecond time.Sleep (delay) fmt.Println (s) func main () fmt.Println ("--- Esegui sequenzialmente come funzioni normali") per i: = 0; io < 4; i++ wg.Add(3) f("1") f("2") f("3") fmt.Println("--- Run concurrently as goroutines") for i := 0; i < 5; i++ wg.Add(3) go f("1") go f("2") go f("3") wg.Wait()
Le goroutine nel programma 1,2,3 non comunicano tra loro o operano su strutture dati condivise. Nel mondo reale, questo è spesso necessario. Il pacchetto "sync" fornisce il tipo Mutex con Serratura()
e Sbloccare()
metodi che prevedono l'esclusione reciproca. Un ottimo esempio è la mappa Go standard.
Non è sincronizzato dal design. Ciò significa che se più goroutine accedono contemporaneamente alla stessa mappa senza sincronizzazione esterna, i risultati saranno imprevedibili. Ma se tutte le goroutine accettano di acquisire un mutex condiviso prima di ogni accesso e lo rilasciano in seguito, l'accesso verrà serializzato.
Mettiamo tutto insieme. Il famoso Tour of Go ha un esercizio per costruire un web crawler. Forniscono un'ottima struttura con un finto Fetcher e risultati che ti consentono di concentrarti sul problema in questione. Consiglio vivamente di provare a risolverlo da solo.
Ho scritto una soluzione completa utilizzando due approcci: una mappa e canali sincronizzati. Il codice sorgente completo è disponibile qui.
Ecco le parti rilevanti della soluzione "sincronizzazione". Per prima cosa, definiamo una mappa con una struttura mutex per contenere gli URL recuperati. Notare la sintassi interessante in cui un tipo anonimo viene creato, inizializzato e assegnato a una variabile in un'istruzione.
var fetchedUrls = struct urls map [stringa] bool m sync.Mutex urls: make (mappa [stringa] bool)
Ora, il codice può bloccare il m
mutex prima di accedere alla mappa degli URL e sbloccare quando è fatto.
// Controlla se questo url è già stato recuperato (o recuperato) fetchedUrls.m.Lock () se fetchedUrls.urls [url] fetchedUrls.m.Unlock () return // OK. Prendiamo questo url fetchedUrls.urls [url] = true fetchedUrls.m.Unlock ()
Questo non è completamente sicuro perché chiunque può accedere a fetchedUrls
variabile e dimentica di bloccare o sbloccare. Un design più robusto fornirà una struttura dati che supporta operazioni sicure eseguendo il blocco / sblocco automaticamente.
Go ha un eccellente supporto per la concorrenza utilizzando goroutine leggere. È molto più facile da usare rispetto ai thread tradizionali. Quando è necessario sincronizzare l'accesso alle strutture dati condivise, Go ha le spalle con il sync.Mutex
.
C'è ancora molto da dire sulla concorrenza di Go. Rimanete sintonizzati…