Smeg: Chi sono? Dementus: Qualcuno competente ed eccessivamente risentito. Smeg: Cosa pensi che vogliano? Dementus: Me, senza il mio equipaggio.
Ed eccoci di nuovo sul pezzo. E, visto che si tratta della seconda parte dell’articolo Fury Go, non posso non agganciarmi allo splendido prequel di Mad Max: Fury Road, e cioè a Furiosa: A Mad Max Saga diretto sempre dal Maestro George Miller. E, in effetti, anche questo articolo è un prequel: nell’altro avevo descritto come spedire una struttura in Go via IPC socket (UNIX domain socket), e il Software l’avevo scritto partendo da una mia versione “base” che inviava solo dei semplici testi. Ed ecco, in questa seconda parte vi mostrerò, come prequel, la versione base, dimostrandovi, come promesso, che le prestazioni buone ma non eccellenti del benchmark erano dovute (spoiler) solo alla codifica della struttura, perché con i soli testi il Go va come un treno!
Ok, e allora andiamo con il prequel, descrivendo la versione base che ho scritto prima di quella che usa le strutture. Siamo pronti? Si? E allora partiamo direttamente con il codice!
// reader.go - main processo figlio: è un reader (un server) su IPC socket package main import ( "bufio" "fmt" "net" "os" "time" ) // funzione main func main() { // start ascolto sul file di scambio "myipcs" (con UNIX domain socket) fmt.Printf("processo %d partito (reader)\n", os.Getpid()) addr := net.UnixAddr{Name: "./myipcs", Net: "unix"} lner, err := net.ListenUnix("unix", &addr) if err != nil { // errore listen fmt.Println(err) return } // prenoto la chiusura del listener e rimuovo (eventualmente) il file di scambio defer lner.Close() defer os.Remove("./myipcs") // accetta connessioni da un writer entrante conn, err := lner.AcceptUnix() if err != nil { // errore accept fmt.Println(err) return } // set time di partenza per calcolare il tempo impiegato start := time.Now() // loop di lettura messaggi dal writer n_msg := 0 connrdr := bufio.NewReader(conn) // reader sulla connessione for { // leggo con il conn reader client_msg, err := connrdr.ReadString('\n') if err != nil { // errore di lettura fmt.Println(err) return } // test numero messaggi per forzare l'uscita n_msg++ if n_msg == 2000000 { // il processo chiude la connessione ed esce per numero raggiunto fmt.Printf("reader: ultimo messaggio ricevuto: %s", client_msg) fmt.Printf("reader: processo %d terminato (messaggi=%d tempo totale:%s)\n", os.Getpid(), n_msg, time.Since(start).Truncate(time.Millisecond).String()) conn.Close() return } } }
// writer.go - main processo figlio: è un writer (un client) su IPC socket package main import ( "fmt" "net" "os" "time" ) // funzione main func main() { // mi assicuro che il writer parta dopo il reader fmt.Printf("processo %d partito (writer)\n", os.Getpid()) time.Sleep(100 * time.Millisecond) // connessione al server remoto sul file di scambio "myipcs" addr := net.UnixAddr{Name: "./myipcs", Net: "unix"} conn, err := net.DialUnix("unix", nil, &addr) if err != nil { // errore dial fmt.Println(err) return } // loop di scrittura messaggi per il reader var my_text string index := 0 for { // test index per forzare l'uscita if index == 2000000 { // il processo chiude la connessione ed esce per indice raggiunto fmt.Printf("writer: processo %d terminato (text=%s messaggi=%d)\n", os.Getpid(), my_text, index) conn.Close() return } // compongo il messaggio e lo invio index++ my_text = fmt.Sprintf("un-messaggio-di-test:%d\n", index) // invio il messaggio al server remoto _, err = conn.Write([]byte(my_text)) if err != nil { fmt.Println("errore di invio: ", err) return } } }
Come avrete notato dalla descrizione nella prima linea (e anche dal codice, spero!) ho usato anche questa volta gli IPC socket. Poi ho anche scritto la versione con i Network Socket, ma non vi mostrerò il codice perché è quasi identico. Effettivamente, per la magia del Go, il codice è semplicissimo rispetto alla analoga versione in C vista qui (in questo caso era la versione “fast”), ed è anche quasi identico alla versione con le strutture dello scorso articolo. Come sempre il codice è stra-commentato, e credo che possa essere facilmente compreso anche da chi non conosce il Go, però mi interessa, a questo punto, aggiungere qualche dettaglio per far notare le (poche) differenze rispetto a quello dello scorso articolo.
Cominciamo, allora, con il writer: in entrambe versioni si crea, inizialmente, un oggetto “connessione” conn:
// connessione al server remoto sul file di scambio "myipcs" addr := net.UnixAddr{Name: "./myipcs", Net: "unix"} conn, err := net.DialUnix("unix", nil, &addr)
Poi, nella versione con le strutture, si invia il messaggio attraverso il codec encoding/gob passandogli la connessione:
// set encoder e spedizione dall'encoder encoder := gob.NewEncoder(conn) err = encoder.Encode(message)
Invece, nella versione base si scrive, direttamente, con l’oggetto connessione creato all’inizio:
// invio il messaggio al server remoto _, err = conn.Write([]byte(my_text))
E ora passiamo al reader: anche qui, in entrambe versioni, si crea un oggetto “connessione” conn:
// accetta connessioni da un writer entrante conn, err := lner.AcceptUnix()
Poi, nella versione con le strutture, si riceve il messaggio attraverso il codec passandogli la connessione:
// set decoder e ricezione dal decoder decoder := gob.NewDecoder(conn) decoder.Decode(&message)
Invece, nella versione base, si legge con un oggetto “Reader” (della libreria bufio) creato sulla connessione vista sopra:
connrdr := bufio.NewReader(conn) // reader sulla connessione for { // leggo con il conn reader client_msg, err := connrdr.ReadString('\n')
Come avrete notato le differenze sono poche ma significative. Notare anche che nella versione con le strutture il testo del messaggio è una stringa dentro la struttura Message, mentre nella versione base il messaggio è direttamente una stringa terminata con un “newline”: questo è un particolare importante, perché in fase di ricezione con la funzione ReadString appena vista è necessario specificare qual’è il terminatore di stringa.
E vabbé, so che siete curiosi, è ora di passare ai risultati! Di seguito i risultati dei benchmark delle “versioni base” che inviano solo testi in Go:
aldo@Linux $ cd ../go-ipcsocketbase/ aldo@Linux $ ./processes sono il padre (15381): attendo la terminazione dei figli sono il figlio 1 (15382): eseguo il nuovo processo sono il figlio 2 (15383): eseguo il nuovo processo processo 15382 partito (reader) processo 15383 partito (writer) writer: processo 15383 terminato (text=un-messaggio-di-test:2000000 messaggi=2000000) reader: ultimo messaggio ricevuto: un-messaggio-di-test:2000000 reader: processo 15382 terminato (messaggi=2000000 tempo totale:3.058s) sono il padre (15381): figlio 15382 terminato (0) sono il padre (15381): figlio 15383 terminato (0) ./processes: processi terminati
aldo@Linux $ cd ../go-socketbase/ aldo@Linux $ ./processes sono il padre (15408): attendo la terminazione dei figli sono il figlio 1 (15409): eseguo il nuovo processo sono il figlio 2 (15410): eseguo il nuovo processo processo 15410 partito (writer) processo 15409 partito (reader) writer: processo 15410 terminato (text=un-messaggio-di-test:2000000 messaggi=2000000) reader: ultimo messaggio ricevuto: un-messaggio-di-test:2000000 reader: processo 15409 terminato (messaggi=2000000 tempo totale:8.46s) sono il padre (15408): figlio 15409 terminato (0) sono il padre (15408): figlio 15410 terminato (0) ./processes: processi terminati
A questo punto i risultati parlano senza temi di smentite: i tempi realizzati sono decisamente migliori di quelli mostrati per le versioni che spedivano strutture complesse, quindi è evidente che la maggior parte del tempo di CPU se la mangiava la libreria specializzata encoding/gob, che funziona bene però, a quanto pare, non è un fulmine. E, grazie ai test appena mostrati sopra si può affermare (come anticipato nella prima parte dell’articolo) che il Go è un linguaggio notevolmente veloce, alla faccia di chi pensa il contrario… Notare che la versione IPC con i suoi 3.058s è addirittura veloce come la versione in C (che impiegava 3.309s ma con messaggi leggermente più lunghi a causa della presenza dell’indice)! Anche in questo caso (come già nello scorso articolo) la versione con i Network socket è un po’ più lenta (8.46s) ma è, comunque, sufficientemente veloce.
Ok, credo che, per il momento si può chiudere la parentesi Go sulla comunicazione tra processi: credo che i risultati siano stati interessanti, specialmente per il fatto di avere confrontato codici analoghi per C e Go. Non so di cosa parlerò` prossimamente: in questo momento ho in mente solo le prossime (meritate) vacanze. Ci sentiremo più avanti, ben rilassati e pronti per nuove avventure in C (o in Go…)!
Ciao, e al prossimo post!
https://italiancoders.it/furiosa-go-come-spedire-una-struttura-in-go-pt-2/