Category Archives: Go

Il linguaggio di programmazione Go, da Google.

L’avvenire del Fortran e i suoi successori

fortran

Forse mi sono lasciato prendere la mano, non so se sia così necessario questo post. Ma siccome l’ho scritto lo pubblico :???:
Tutto è partito da qui: Why Scientists Are Still Using FORTRAN in 2014 che rimanda a Ars technica, qui: Scientific computing’s future: Can any coding language top a 1950s behemoth? dove troviamo

But almost universally, the language in which these simulation codes are written is Fortran, a relic from the 1950s.

e

Wherever you see giant simulations of the type that run for days on the world’s most massive supercomputers, you are likely to see Fortran code.

Poi dice che subito dopo viene inventato il Lisp. Che non ha successo per la sua stranezza:

Weirdness, because the prefix notation used by Lisp made expressions in that language look a lot less like normal mathematics than did math rendered in Fortran. (Fortran stood, after all, for FORmula TRANslator.) A normal chemist or engineer is far more comfortable with y = (a + b)/c than with (setf y ((/ (+ a b) c))).

Ok, l’ho sentita tante volte che mi verrebbe voglia di ignorarla. Invece no! Perché è (quasi) tutto lì. Davvero, ve ne faccio il riassunto.

(setf y (/ (+ a b) c))

Parto dalla parentesi più interna: (+ a b); questa è una lista il cui primo elemento è una funzione. Questa funzione opera sui successivi elementi della lista e li somma. Questa somma (chiamiamola A+B viene restituita: Lisp restituisce sempre l’ultima espressione calcolata.
Quindi la nostra espressione diventa:

(setf y (/ A+B c))

dove (/ A+B c) è una lista in cui il primo elemento è la funzione / che restituisce il rapporto del secondo elemento della lista per tutti i successivi.

Il valore restituito viene assegnato alla variabile y, tramite setf (setf sta per set (quote e sì, si può scrivere anche setq o set ' –non sono esattamente la stessa cosa ma il risultato è lo stesso).

Uh! visto? il Lisp è uniforme. Si comporta sempre allo stesso modo: il primo elemento della lista (chiamato CAR o first) indica come trattare il resto della lista (CDR o rest) e restituisce il risultato.
Per cui il Lisp è fatto da liste dentro liste dentro liste ancora.  Tutto lì (ok, sto semplificando). Non so voi ma a me sembra molto semplice.

Per contro il Fortran riproduce la sintassi matematica che si usa a scuola:

y = (a + b) / c

Sì, lo ammetto, siamo abituati così e per un calcolo semplice come questo meglio il Fortran. Ma per un caso non banale si vede la differenza: con il Lisp ho una concatenazione di liste e con il Fortran una serie di assegnazioni (attenzione il segno = si deve leggere “diventa“, dice il Wirth (che scrive :=)).

11c

Poi una cosa ancora: io sono vecchio e non so nemmeno se si usano ancora ma quando frequentavo il Poli, roba anni ’70, i migliori usavano le calcolatrici HP, con RPNreverse polish notation– quasi come il Lisp (cioè no, esattamente il contrario), nel nostro caso si sarebbe scritto

a
b
+
c
/

Con risparmio di digitalizzazione di tasti –non servono le parentesi–, si consumano meno i diti e –sopratutto– i risultati intermedi sono disponibili nello stack (mi dicono che dovrei usare il termine “pila”).

Ma tornando alla programmazione allora quando è uscito il Fortran era una cosa nuova di pakka, un compilatore per linguaggio facile per gli umani, alternativo alle criptiche istruzioni assembly. E sui calcolatori computer c’era solo il Fortran. Molto diverso da quello di oggi, istruzioni molto elementari, quasi da assembly. E tanti GOTO, spesso mascherati come IF.

Ok, fine dello sfogo, torno al post di Ars Technica secondo il quale ci sono tre candidati a sostituire il Fortran:

Haskell, bello, sintassi da matematici, funzionale (cioè odia le variabili), chissà;
Clojure, Lisp & JVM, sì c’è Java sotto; ho scritto qualcosa in passato;
Julia, nuovissimo, ne so molto poco.

Ma ce ne sono altri, secondo me: il C++, Java, Python e altri ancora. Tutti ampiamente utilizzati.
E i nuovissimi Go e Rust. Di Go sono a conoscenza di un programma funzionante, per il Web, Rust promette bene (ne parla spesso robitex su questo blog).
Inoltre proprio ieri vosto questo post: #golang: The Next Great Teaching Language. Il blog è nuovo ma promette bene. E il post è illuminante.

Invece il Fortran non è amato dai giovani (quelli che conosco), lo trovano vecchio, inusuale. E se possono lo sostituiscono con qualcosa di più pratico, per esempio Matlab (a volte basta la versione free Octave). Ma sto parlando di chi usa il computer come un mezzo complementare. E lo usa per AutoCAD, fogli di calcolo Excel e come macchina da scrivere.

Mi mancano i programmatori di una volta, quando il mondo era nuovo ;-)

Precisione iii

Corto circuito

Questo post è un bellissimo corto circuito: conosco Orlando perché è un amico del GuIT che tra l’altro ho conosciuto l’anno scorso di persona al GuIT meeting di Napoli. E proprio Orlando ha commentato il post di Juhan Precisione su questo stesso blog, di qualche giorno fa su alcuni calcoli.
Questo dimostra che gli appassionati non vedono l’ora di scoprire nuove cose sul proprio argomento preferito.

Chiudo il circuito

Eccomi dunque a chiudere il circuito informatico presentrando un programma in Go che esegue le stesse operazioni eseguite in awk ed in Fortran:

package main

import (
    "fmt"
    "math/big"
)

func main() {
    s, u := big.NewInt(2), big.NewInt(1)
    for i := 1; i <= 10; i++ {
        // a = s - 1
        a := big.NewInt(0).Set(s).Sub(s, u)
        // s*a + 1 = s*(s - 1) + 1
        s.Mul(a, s).Add(s, u)
        fmt.Println(i, s)
    }
}

Tecnicamente, vorrei farvi notare che è possibile in Go concatenare le chiamate dei metodi come per esempio si legge alla riga 12 del codice sorgente, se il metodo stesso ritorna lo stesso oggetto.

Ho utilizzato il pacchetto dei grandi numeri big necessario perché dopo la sesta iterazione i numeri superano il limite massimo del tipo int64. Certo avrei potuto utilizzare il tipo uint64 ma potevo chiudere il circuito soltanto esagerando alla grande portando il ciclo fino a 10 iterazioni… ecco di seguito il risultato:

1 3
2 7
3 43
4 1807
5 3263443
6 10650056950807
7 113423713055421844361000443
8 12864938683278671740537145998360961546653259485195807
9 165506647324519964198468195444439180017513152706377497841851388766535868639572406808911988131737645185443
10 27392450308603031423410234291674686281194364367580914627947367941608692026226993634332118404582438634929548737283992369758487974306317730580753883429460344956410077034761330476016739454649828385541500213920807

A questo punto, ci sta propio bene un
Alla Prossima!
R.

Il termine dell’appalto

Sommario

Calcoleremo la data di termine di un appalto a partire da quella di inizio lavori dal numero di giorni naturali e consecutivi stabiliti dal contratto, utilizzando due linguaggi di programmazione: Lua e Go.

L’occasione

Potevamo utilizzare un banale foglio di calcolo per determinare la data del termine di fine lavori per un contratto d’appalto? No… No perché ci si annoia sempre a fare le stesse cose e sentiamo la necessità di migliorarci.

Calcolo

La descrizione del calcolo che seguiremo è presto fatta: alla data di inizio sommiamo il numero di giorni stabiliti per completare l’opera e togliamo un giorno, per ottenere così la data cercata corrispondente al termine dei lavori.

Infatti, il giorno della consegna conta già uno ai fini del tempo contrattuale. Possiamo vedere la cosa considerando l’istante di tempo della mezzanotte: è al contempo il primo istante del giorno ma anche l’ultimo di quello precedente.
Tuttavia la mezzanotte è sempre interpretato come il primo istante del giorno successivo. Per la data di inizio è corretto ma non per la data di termine.

Lua

In Lua, utilizzando le funzioni della libreria interna “os”, costruiamo la data di inizio lavori che viene gestita come il numero di secondi trascorsi da un certo momento (il classico epoch Unix). Sommiamo poi il numero di secondi corrispondenti alla durata dell’appalto, sottraendovi un secondo per ottenere l’attimo prima della fine dell’ultimo giorno di lavoro.
La funzione termina restituendo la stringa della data formattata eventualmente come specificato nell’ultimo argomento facoltativo.

-- restituisce la data di scadenza dell'appalto
-- nota la data di consegna ed il numero di
-- giorni naturali e consecutivi della durata
-- dei lavori contrattuali

local function endingDate(d,m,y, days, frmt)
    local secsperday = 24*60*60
    local t_start = os.time{day=d,month=m,year=y,hour=0}
    local t_end   = t_start + days*secsperday -1
    frmt = frmt or "%d/%m/%Y"
    return os.date(frmt, t_end)
end

print(endingDate(26,8,2013, 60))

Go

Anche in Go utilizziamo la libreria disponibile con il linguaggio, in particolare il pacchetto “time”, per restituire nello stesso formato precedente la data risultato.

package main

import (
    "time"
    "fmt"
)

func main() {
    fmt.Println(endingDate(2013,8,26, 60))
}

func endingDate(y int, m time.Month, d, days int) string {
    s := time.Date(y, m, d, 0, 0, 0, 0, time.UTC)
    e := s.AddDate(0, 0, days).Add(-time.Second)
    return fmt.Sprintf("%d/%02d/%d",e.Day(), e.Month(), e.Year())
}

Finale

Le differenze con Lua possono sembrare ad un primo sguardo minime, ma non è così anche se entrambi hanno feature in comune come il supporto alla concorrenza, le funzioni di prima classe e le closure.

Io, la prima cosa che ho notato è la maggiore precisione della libreria “time” di Go rispetto a Lua (che si riflette nel modo in cui percepiamo le date), e l’uso trasparente in Go di tipi sinonimi di quelli di base come per esempio nella rappresentazione del mese — il tipo time.Month per intenderci.

Penso però che la differenza sostanziale sia nell’importanza che il linguaggio da ai tipi: in Lua la tipizzazione è dinamica come si conviene per un linguaggio di scripting mentre in Go è statica: nel codice occorre sempre definire il tipo di dato e solo rispetto a questa classificazione i dati possono essere elaborati, per esempio da una funzione).

Il risultato è che in Go si è maggiormente portati a pensare concettualmente ai dati come termini del problema, strutturando il codice. Ovvio che questa differenza è la stessa che troviamo confrontando qualsiasi coppia di linguaggi con tipi dinamici e statici (come Python e Java), ma il Go è certamente uno dei linguaggi a tipi statici che si avvicinano di più ai linguaggi a tipi dinamici…

Un saluto.
R.

Leggere un foglio di calcolo .ods in Go

Sommario

Una breve descrizione di come leggere il contenuto di un foglio di calcolo nel formato Open Document Format di estensione .ods con una piccola libreria in Go.

Obiettivo

Dato un foglio di calcolo di Libre Office od anche di Apache Open Office vorremmo leggere il contenuto della cella “A1″ della prima tabella, utilizzando il Go, il linguaggio open source creato da Google.

I prerequisiti per poter essere operativi e tentare la sfida sono: un file nel formato OpenDocument, standard ISO, di un foglio di calcolo naturalmente, ed una installazione di Go per il vostro sistema operativo.

Un piccolo trucco

Sappiamo che il file di estensione .ods è in realtà un file compresso contenente dati xml. Occorre quindi decomprimere il file, leggere all’interno il file relativo ai dati ed interpretare il codice testuale xml.

Ebbene, il piccolo trucco consiste proprio in questo: procurarsi una libreria già pronta che fa tutto questo per noi. Guarda caso una libreria così esiste davvero ed è ospitata su github e messa gentilmente a disposizione per l’uso e lo sviluppo libero da knieriem.
Lo sviluppo comunitario è qualcosa di incredibile anche per il numero elevatissimo di progetti, qualcosa che a pensarci mi sorprende sempre. Grazie davvero.

La libreria odf contiene pochissimi file ed è, al momento, praticamente priva di documentazione, ma può leggere e scrivere fogli di calcolo nel formato aperto della collezione OpenDocument.

Per installarla un semplice comando farà al caso nostro e se va tutto bene ci vuole veramente un attimo:

 $ go get github.com/knieriem/odf

Il codice di lettura del file ods

Bene, adesso tocca a noi!
Diamo al file del foglio elettronico il nome di “test.ods” (questo nome lo ritroveremo nel sorgente) e posizioniamolo in una directory assieme al nostro sorgente che per adesso è ancora un file vuoto.

La prima cosa che dovrà fare il programma, a regola, è aprire il file in lettura. Dunque proviamo questo primo codice in Go (prima di poterlo eseguire naturalmente occorre compilarlo):

package main

import (
	"github.com/knieriem/odf/ods"
	"fmt"
	)

func main() {
	f, err := ods.Open("test.ods")
	if err != nil {
		fmt.Println(err)
		return
	}
	defer f.Close()
}

Il programma non fa ancora nulla: semplicemente apre il foglio elettronico ed esce, e serve solo per provare che sia tutto a posto e per cominciare, perché no, a prendere confidenza con la sintassi del Go.

Leggere il contenuto della cella

Siamo pronti per chiudere la sfida (a nostro favore ovvio). Il prossimo passo una volta aperto il file è quello di leggerne il contenuto. La libreria odf mette a disposizione la funzione ParseContent() che accetta come parametro il puntatore ad una struct documento e restituisce un eventuale errore.
Per saperne di più sui puntatori basta leggere i precedenti post sull’argomento in particolare questo.

Disponendo ora del documento possiamo accedere tramite alcuni metodi (ancora in corso di esplorazione da parte mia), ai dati del foglio elettronico ed in particolare possiamo osare nell’ordine a fare:

  1. stampare il numero delle tabelle contenute nel file;
  2. stampare il nome del primo foglio di calcolo;
  3. stampare (finalmente) il contenuto della cella A1;

Ecco il listato completo che chiude questa breve esplorazione.

Ma prima un saluto a tutti, ed in particolare a Juhan che non credo possa fare altrettanto con i linguaggi che frequenta lui: uno ha il nome di un temibile serpente, un altro somiglia ad una pietra preziosa di colore rosso, eccetera.
:-)

package main

import (
	"github.com/knieriem/odf/ods"
	"fmt"
	)

func main() {
	f, err := ods.Open("./test.ods")
	if err != nil {
		fmt.Println(err)
		return
	}
	defer f.Close()

	// parsing del contenuto del file
	var doc ods.Doc
	if err := f.ParseContent(&doc); err != nil {
		fmt.Println(err)
		return
	}

	// stampa il numero di tabelle
	fmt.Println(len(doc.Table))

	// stampa il nome del primo foglio
	fmt.Println(doc.Table[0].Name)

	// stampa cella "A1"
	firstrow := doc.Table[0].Strings()[0]
	fmt.Println(firstrow[0])
}

Alberelli in Go

Sommario

Svolgimento dell’esercizio di implementazione di un albero con funzioni ricorsive in linguaggio Go.
Attratto da questo post su questo stesso blog di dikiyvolk, ho provato a reimplementare le funzioni di base per la struttura dati nel nuovo linguaggio targato Google, anziché in Delphi, come la versione originale, anche perché Juhan aveva citato proprio il Go nei commenti…
La visita dell’albero produce la stampa ordinata dei valori memorizzati nell’albero.

L’albero in Go

Il codice che vi riporto tutto d’un fiato di seguito, è molto simile a quello che si sarebbe scritto in C od in C++. Per eseguirlo cliccate su questo link del Go Playground e attivate il pulsante Run, oppure installate Go e date il comando di compilazione e poi quello di esecuzione intendendo che il file sorgente sia stato chiamato otree.go(adattate per il caso di sistemi operativi non derivati da unix):

$ go build otree.go
$ ./otree

Bando alle ciance, ecco il codice:

package main

import "fmt"

func main() {
    // setup a new tree
    tree := NewTree(10, 6, 4, 4, 3, 8, 12)
    tree.Visit()
    // adding a new element
    tree.AddData(9)
    fmt.Println()
    tree.Visit()
    // test an empty tree
    tree2 := NewTree()
    fmt.Println()
    tree2.Visit()
}

type OTree struct {
    root *leaf
}

type leaf struct {
    data        int
    left, right *leaf
}

func NewTree(nargs ...int) *OTree {
    ot := new(OTree)
    ot.AddData(nargs...)
    return ot
}

func (ot *OTree) AddData(nargs ...int) {
    if ot.root == nil && len(nargs) > 0 {
        ot.root = new(leaf)
        ot.root.data = nargs[0]
        nargs = nargs[1:]
    }
    for _, val := range nargs {
        addinteger(ot.root, val)
    }
}

func (ot *OTree) Visit() {
    if ot.root == nil {
        fmt.Println("The tree is empty.")
    } else {
        visit(ot.root)
    }
}

// core functions
func addinteger(t *leaf, n int) {
    switch {
    case n > t.data:
        if t.right == nil {
            t.right = new(leaf)
            t.right.data = n
        } else {
            addinteger(t.right, n)
        }
    case n < t.data:
        if t.left == nil {
            t.left = new(leaf)
            t.left.data = n
        } else {
            addinteger(t.left, n)
        }
    default:
        // duplicated insertion denied
    }
}

func visit(t *leaf) {
    if t.left != nil {
        visit(t.left)
    }
    fmt.Println(t.data)
    if t.right != nil {
        visit(t.right)
    }
}

Un saluto.
Alla prossima.
R.

Compilazione LaTeX in pool

Sommario

Con il linguaggio Go, studieremo un pool di goroutine che si occuperà di snellire un po’ il lavoro di compilazione di molti file sorgenti LaTeX.

Situazione

In una cartella ho salvato 222 file sorgenti LaTeX relativi a schede tecniche (generate in automatico). Il passo successivo è quello di compilare i sorgenti con il compositore pdflatex per produrre i corrispondenti 222 file pdf.

Quale occasione migliore per sperimentare le doti di esecuzione concorrente del linguaggio Go?

Funzionamento

Poiché la procedura in Go non può fare altro che lanciare il comando di sistema esterno — che vedremo or ora — andremo a sovraccaricare le risorse del sistema operativo.
Meglio quindi escludere la soluzione di dedicare ad ogni compilazione una goroutine, per lavorare invece con un piccolo numero di linee di esecuzione indipendente che prelevano uno dopo l’altro nomi di file LaTeX.
Come sanno bene gli utenti TeX, il comando di compilazione da impartire in console, o terminale è il seguente:

$ pdflatex nomedelfile

Naturalmente occorre che sul sistema sia installata una distribuzione TeX come per esempio TeX Live.
Per fare la stessa cosa in Go, il frammento di codice seguente mostra come utilizzare il pacchetto os/exec della ricca libreria del linguaggio per lanciare il comando esterno.

// caricamento pacchetto
import "os/exec"

// in qualche funzione
filename := "nomefile" // nome di esempio

// preparazione del comando
cmd := Command("pdflatex", filename)
// esecuzione del comando esterno
err := cmd.Run()
if err != nil {
    fmt.Println(err)
}

Pool di goroutine (di nuovo ah ah)

// pool di goroutine
package main

import (
    "fmt"
    "os"
    "os/exec"
    "path/filepath"
    "runtime"
    "time"
)

const dirpath = "schedetecniche"

var cpu int = runtime.NumCPU()
var workers int = cpu

func main() {
    t := time.Now()
    runtime.GOMAXPROCS(cpu)
    tfiles := getTeXFileNames()
    done := compileTeXFiles(tfiles)

    for i := 1; i < len(tfiles)+1; i++ {
        fmt.Printf("Compilazione [%d] in %v\n", i, <-done)
    }
    fmt.Println("Tempo totale: ", time.Since(t))
    fmt.Print("Premere invio")
    var a string
    fmt.Scanln(&a)
}

func compile(done chan<- time.Duration, filename string) {
    opt := fmt.Sprintf("-output-directory=%s", dirpath)
    file := dirpath + "/" + filename
    cmd := exec.Command("pdflatex", opt, file)
    start := time.Now()
    err := cmd.Run()
    if err != nil {
        fmt.Println(err)
    }
    done <- time.Since(start)
}

func compileTeXFiles(f []string) <-chan time.Duration {
    done := make(chan time.Duration, workers)
    files := make(chan string, workers)
    go func() {
        for _, fn := range f {
            files <- fn
        }
        close(files)
    }()
    for i := 0; i < workers; i++ { // goroutine pool
        go func() {
            for filename := range files {
                compile(done, filename)
            }
        }()
    }
    return done
}

func getTeXFileNames() (s []string) {
    s = make([]string, 0, 1000)

    wf := func(path string, info os.FileInfo, err error) error {
        fn := info.Name()
        if ext := filepath.Ext(fn); ext == ".tex" {
            s = append(s, fn)
        }
        return nil
    }
    err := filepath.Walk(dirpath, wf)
    if err != nil {
        fmt.Println(err)
    }

    return
}

Un dato

Su una macchina multicore ad 8 thread, il programma descritto è circa il 75% più veloce che non quello con la compilazione sequenziale…

Alla prossima…
Un saluto.
R.

Destinazione Go 1.1…

Sommario breve

Come compilare il compilatore per il linguaggio Go su Debian e derivate (come Ubuntu)…

Antefatto

Era parecchio tempo che non smanettavo con i sorgenti e la compilazione di pacchetti. Me ne ha data l’occasione la recente uscita della versione 1.1 del linguaggio Go.

Il mio PC è un po’ datato infatti ed il processore non supporta le istruzioni che sono state aggiunte nelle generazioni successive, in particolare è in difficoltà con le istruzioni SSE.

Quindi un post di quelli classici di una volta, della serie “come fare per” che oggi mi capita sempre meno di scrivere per effetto dell’aumento della documentazione disponibile.

Per compilare Go dai sorgenti su un PC Linux del resto ho seguito la guida ufficiale Installing Go from source sul sito del Go.

Questo l’errore che ricevo invece se installo i pacchetti precompilati nel momento di lanciare il compilatore:

SIGILL: illegal instruction PC=0x81832bd  math.init·1() 	/usr/local/go/src/pkg/math/pow10.go:34 +0x1d
... ecc

Passi della procedura

Per prima cosa attrezziamoci con il compilatore C, le sue librerie e con mercurial:

$ sudo apt-get install gcc libc6-dev
$ sudo apt-get install mercurial

Scarichiamo i sorgenti con (vi allego anche i messaggi di output) nella directory Home:

$ hg clone -u release https://code.google.com/p/go
directory di destinazione: go
sto richiedendo tutte le modifiche
sto aggiungendo i changeset
sto aggiungendo i manifesti
sto aggiungendo le modifiche ai file
aggiunti 17004 changeset con 59801 modifiche a 8126 file (+6 head)
updating to branch release-branch.go1.1
3736 files updated, 0 files merged, 0 files removed, 0 files unresolved

A questo punto è preferibile valorizzare tre variabili d’ambiente per impostare la directory principale, così da non disturbare l’installazione della legacy della versione precedente 1.0.3, e soprattutto il tipo di processore (attenzione, è bene ricordare che questi valori valgono solamente nella sessione corrente del terminale, ok?).

$ export GOROOT=$HOME/go
$ export GOOS=linux
$ export GOARCH=386

Ok, siamo pronti:

$ cd $GOROOT/src
$ ./all.bash

Dopo circa 10 minuti terminerà la compilazione ed inizierà la fase di testing che impiega molto più tempo. Alla fine vi apparirà il nuovo e velocissimo Go1.1 correttamente funzionante… ma sul mio PC lento lento che me ne faccio? Bah ;-)

...
ALL TESTS PASSED

---
Installed Go for linux/386 in /home/roberto/go
Installed commands in /home/roberto/go/bin
*** You need to add /home/roberto/go/bin to your PATH.
$ cd $GOROOT/bin
$ ./go version
go version go1.1 linux/386

R.

Pool di goroutine

Sommario

Con il linguaggio Go, studieremo come implementare un pool di goroutine che si occupa di eseguire un insieme di compiti ciclicamente prelevando i dati da un unico canale di ingresso ed inserendo i risultati in un secondo canale di uscita.

Impiegati instancabili

Immaginiamo un gruppo di impiegati che in un unico grande ufficio evade una pila di pratiche una alla volta. Il capo deposita le pratiche sempre una alla volta in una pila, gli impiegati facendo la coda prelevano a turno dal fondo della pila una pratica, tornano alla scrivania per lavorarci e quando hanno finito la depositano in una pila di uscita per poi tornare a prelevare una nuova pratica.
Vedremo che le regole di questo ufficio sono alquanto strane: gli impiegati non si riposano mai nemmeno quando le pratiche sono tutte evase e la pila è vuota!

Tante gooutine si mettono in lista e prelevano il lavoro da fare...

Tante gooutine si mettono in lista e prelevano il lavoro da fare…

Pool di goroutine

Ok, niente panico! Passiamo a definire la geometria di un pool di gouroutine che si occupa di eseguire compiti. Ciascuna di esse preleva un dato di ingresso dal canale comune a tutte, ed elabora i risultati, li immette nel canale di uscita e ricomincia da capo.
Per concretizzare, mettiamo il caso in cui si voglia calcolare il quadrato di alcuni numeri.
A questo scopo, creiamo una funzione che spedisca uno alla volta in un canale i dati:

// arriva il capo ufficio!
func sending(data []int) chan int {
    ch := make(chan int, 10)
    go func () {
        for _, n := range data {
            ch <- n
        }
        close(ch)
    }()
    return ch
}

La simpatica funzione sending() restituisce il canale in cui inserisce i numeri tramite una goroutine così che quando il canale non è pronto essa rimarrà nello stato di blocco e non la goroutine principale con il rischio di generare un deadlock, a seconda dell’ordine di setup del pool.
Il deadlock è un errore di runtime: il programma in esecuzione si interrompe quando tutte le goroutine non possono proseguire perché bloccate.
Per sperimentare un deadlock, provate questo programma:

package main

func main() {
    ch := make(chan int)
    ch <- 10 // deadlock!
    println(<-ch)
}

Sembra tutto a posto, ma quando spediamo 10 sul canale, la goroutine principale — nella quale sta girando la funzione main() — non può proseguire all’istruzione successiva: entra in blocco nell’attesa che qualcosa richieda un dato all’uscita del canale.
Se definiamo il canale con una capacità maggiore di zero — canale bufferizzato — invece tutto andrà bene.

Tornando all’esempio, rimane da scrivere la funzione che crea il pool di goroutine, eccola:

// assunzione degli impiegati!
func makepool(in chan int, dim int) chan int {
    out := make(chan int) 
    for i := 0; i < dim; i++ {
        go func() {
            dojob(out, in)
        }()
    }
    return out
}

Ciascuna funzione del pool lavora con un ciclo infinito in cui ogni volta si preleva un numero dal canale d’ingresso — alimentato da sending() — e lo si spedisce su quello d’uscita dopo averlo elevato al quadrato:

// gli implacabili impiegati!
func dojob(out chan int, in chan int) {
    for {
        n := <- in 
        out <- n * n
    }
}

La funzione main() orchestra il tutto, prima costruendo ed alimentando il canale di entrata dei dati, poi costruendo il pool di 10 goroutine di elaborazione e per ultimo, attendendo che tutti i risultati arrivino:

func main() {
    // costruzione dello slice di dati
    n := 100
    data := make([]int, n)
    for i := 0; i < n; i++ {
        data[i] = i + 1
    }
    
    // pool's setup
    in := sending(data)
    out := makepool(in, 10)
    
    // waiting for the ending of entire job
    for i := 0; i < n; i++ {
        fmt.Printf("Result %d\n", <- out)
    }
}

Il codice completo pronto per l’esecuzione lo si può trovare a questo link.

Un difetto sottile

Le goroutine del pool operano con un ciclo infinito. Ciascuna di esse tenta di ricevere dati dal canale d’ingresso in che ad un certo punto però viene chiuso una volta terminato l’invio del set di dati.
Poiché in Go, quando si riceve un dato da un canale chiuso non si ottiene un errore ma il valore zero relativo al tipo del canale (in questo caso interi quindi il numero 0), cosa impedisce alle goroutine del pool di continuare a lavorare, almeno fino a quando non termina la goroutine principale?

Nel listato il canale di output out non è bufferizzato, quindi in main() una volta ricevuti i dati attesi viene bloccato. Una goroutine del pool può ancora fare in tempo a calcolare un quadrato ed a richiedere l’invio nel canale di uscita.

Se invece il canale di uscita ha una capacità maggiore di zero, il pool può ancora riempirlo lavorando inutilmente con i valori zero provenienti dal canale di ingresso ormai chiuso, ammettendo che la funzione main() sia ancora impegnata in qualche altro compito, lasciando quindi tempo al pool.

Per riordinare le idee con del codice effettivo, proviamo a verificare cosa succede se continuamo a prelevare dati da un canale chiuso:

package main

func main() {
    ch := make(chan int, 2)
    ch <- 100
    ch <- 200
    close(ch)
    println(<-ch) // stampa 100
    println(<-ch) // stampa 200
    println(<-ch) // stampa 0
}

Possiamo accorgerci però che il canale è chiuso perché ricevere da esso comporta in realtà ottenere non uno ma due valori: il dato utile ed un valore booleano che è vero se il canale è aperto, falso viceversa, esattamente come nel caso della richiesta di una valore in una mappa. Nel seguente programma prima di stampare il valore ci chiediamo se il canale è chiuso e non stiamo per caso ricevendo anziché un dato effettivo solo il valore zero del tipo.
In Go ogni variabile è SEMPRE inizializzata al valore zero corrispondente al tipo, e questa è una grande differenza rispetto ai liguaggi dinamici tipo Lua o Python.

package main

func main() {
    ch := make(chan int, 2)
    ch <- 100
    ch <- 200
    close(ch)
    for val, isOpen:=<-ch;isOpen;val, isOpen = <-ch{
        println(val)
    }
}

Adesso modifichiamo il codice principale nell’intento di rilevare il lavoro ‘imprevisto’ delle goroutine. Mettiamo in pausa per qualche millisecondo la funzione principale prima che termini per dare un po’ di tempo alle goroutine del pool di riempire il canale di uscita di cui è stata aumentata la capacità.
Poi un nuovo canale di interi cycles ci servirà per contare il numero di cicli effettivi compiuti dal pool:

// Simple goroutine pool
package main

import (
    "fmt"
    "time"
)

func main() {
    // costruzione dello slice di dati
    n := 100
    data := make([]int, n)
    for i := 0; i < n; i++ {
        data[i] = i + 1
    }

    // pool's setup
    in := sending(data)
    out := makepool(in, 10)

    // waiting for the ending of job
    for i := 0; i < n; i++ {
        fmt.Printf("Result %d\n", <-out)
    }
    time.Sleep(200 * time.Millisecond)
}

func makepool(in chan int, dim int) chan int {
    out := make(chan int, dim)
    cycles := make(chan int, dim)
    c := 0
    go func() {
        for {
            c += <-cycles
            fmt.Println(c)
        }
    }()
    for i := 0; i < dim; i++ {
        go func() {
            for {
                n := <-in
                out <- n * n
                cycles <- 1
            }
        }()
    }
    return out
}

func sending(data []int) chan int {
    ch := make(chan int, 10)
    go func() {
        for _, n := range data {
            ch <- n
        }
        close(ch)
    }()
    return ch
}

L’esecuzione di questa prova stampa i numeri dei cicli fino a 110. Il pool ha continuato a lavorare fino a saturare il canale di uscita dei risultati, tutti pari a zero. Se richiedessimo la stampa di ulteriori dati dal canale out per controllare che i valori siano zero, si libererebbo dei posti che il pool riempirebbe di nuovo con valori zero, sempre se solo la funzione main() non termini prima.

Soluzione

Abbiamo studiato nei dettagli il problema di un pool di goroutine che tenta di ricevere dati da un canale anche se chiuso. La soluzione è semplicemente quella di modificare il ciclo infinito per tener conto dello stato del canale: nel momento in cui una goroutine del pool scopre che è il canale è chiuso allora può terminare:

func dojob(out chan int, in chan int) {
    for n := range in {
        out <- n * n
    }
}

Alla prossima…
Un saluto.
R.

Giocando a ping pong in Go

Il sommario

Approfondiremo il funzionamento delle goroutine del linguaggio Go studiando l’esempio del ping pong, ovvero due goroutine che si rimbalzano dati sullo stesso canale.
L’argomento è particolarmente interessante perché il supporto diretto alla programmazione concorrente offerto dal Go tramite goroutine e canali, promette di rendere semplice accedere alla potenza di elaborazione dei moderni dispositivi multicore.
L’esempio del ping pong — presentato recentemente anche al Google I/O 2013 — è da una parte semplice in modo che si possono capire i concetti della dinamica di esecuzione concorrente e, dall’altra, offre la possibilità di studiare in dettaglio il comportamento del programma evidenziandone le sottigliezze.

Il codice del ping pong

Riporto 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 indipendentemente la funzione player() non appena creato il canale di comunicazione tra le due. A questo punto sia la prima goroutine chiamata Ann, sia la seconda chiamata Bob sono bloccate perché nessun dato è ancora disponibile nel canale. Ad iniziare il gioco ci pensa l’istruzione successiva che spedisce il numero 1 nel canale.
Da questo momento cosa accade?

package main

import (
    "fmt"
    "time"
)

func main() {
    ball := make(chan int)
    go player("Ann", ball)
    go player("Bob", ball)

    ball <- 1 // start the match
    time.Sleep(10 * time.Second)
    <-ball // stop the match
}

func player(name string, ball chan int) {
    for {
        touch := <- ball
        fmt.Printf("Player %s: %d\n", name, touch)
        touch++
        ball <- touch
        time.Sleep(100 * time.Millisecond)
    }
}

Quello che accade è riportato nello schema seguente: le due linee verticali rappresentano lo stato delle due goroutine Ann e Bob, con lo scorrerere del tempo, ipotizzando che le istruzioni vengano eseguite in un tempo zero. Una croce sulla linea significa che la goroutine è bloccata, un tratto spesso significa che la goroutine è in attesa.
Le frecce indicano dati inviati sul canale ed infine a destra è riportata la stampa in console prodotta dal programma.

Schema dinamico per goroutine

Schema dinamico per le goroutine che giocano a ping pong… i tratti spessi sono i periodi in cui la goroutine è in idle, i tratti con la crocetta sono periodi in cui la goroutine è bloccata.

Lo schema risponde alle domande spontanee: perché al tempo zero entrambi i giocatori danno un tocco alla palla per poi darne uno alternativamente ogni 100 millisecondi?
Come fare per ottenere invece un gioco regolare fin da subito?
Ed ancora, perché se nella funzione principale prelevo un dato dal canale il gioco si ferma?

Risposte spontanee

Quando Ann riceve il primo numero (1) seguendo il codice si ricava che essa stampa 1 e spedisce 2 sul canale e poi si mette in attesa per 100 miliisecondi. Bob è in attesa sul canale, riceve immediatamente (o quasi) il 2, lo stampa ma si blocca all’istruzione che invia 3 sul canale perché Ann sta dormendo. Finalmente Ann si svegli per t=100ms, Bob può quindi spedire il dato ed entrare in letargo per i prossimi 100ms.
Nel frattempo Ann ha già stampato 3 ma non può preseguire perché il canale è bloccato almeno fino a t=200ms. E così via.
All’inizio, come potete notare eseguendo il programma, ci sono due rimbalzi ma poi il gioco si fa regolare per effetto del blocco del canale quando l’altra goroutine dorme…

Per ottenere un gioco regolare fin dall’inizio basta anticipare la messa in attesa della funzione rispetto all’invio sul canale, così (fate per esercizio lo schema dinamico corrispondente e confermatene la correttezza eseguendo il programma):

func player(name string, ball chan int) {
    for {
        touch := <- ball
        fmt.Printf("Player %s: %d\n", name, touch)
        time.Sleep(100 * time.Millisecond)
        touch++
        ball <- touch
    }
}

Infine, se è la funzione main() a prelevare il dato dal canale sia Ann che Bob si metteranno in attesa di nuovo sulla prima istruzione del ciclo for infinito. La funzione principale viene eseguita essa stessa in una goroutine, ed esegue una vera e propria intercettazione della palla.
Per dimostrare con un programma questo meccanismo consideriamo il seguente codice:

// ping pong test
package main

import (
    "fmt"
    "time"
)

func main() {
    ball := make(chan int)
    go player("Ann", ball)
    go player("Bob", ball)
    
    fmt.Println("Start the match")
    ball <- 1
    time.Sleep(10 * time.Second)
    fmt.Println("Pause the match for five seconds")
    tmp := <-ball
    time.Sleep(5 * time.Second)
    fmt.Println("Ok. Go again now")
    ball <- tmp
    time.Sleep(10 * time.Second)
    fmt.Println("Stop!")
    <-ball
}


func player( name string, ball chan int) {
    for {
        touch := <- ball
        fmt.Printf("Player %s: %d\n", name, touch)
        time.Sleep(1*time.Second)
        touch++
        ball <- touch
    }
}

Ok, un esercizio

Cosa succede se faccio giocare tre giocatori invece che due?
Anche in questo caso uno schema temporale di esecuzione come quello proposto chiarisce il comportamento del programma: ad ogni intervallo (per esempio i soliti 100ms iniziali) ci sono due giocatori che fanno un rimbalzo quindi il conteggio è doppio rispetto al caso precedente.
Fate la prova!

Ping pong multigiocatori

Se volessimo far giocare 10 giocatori l’idea potrebbe essere quella di collegarli tramite canali a formare un cerchio. Ciascun giocatore riceve la palla da quello alla sua destra e la rimanda a quello alla sua sinistra.
Ecco il curioso programma (ovvio che vi lascio verificare il risultato):

// ping pong test
package main

import (
    "fmt"
    "time"
)

const p = 10

func main() {
    // creo p canali
    var chs [p]chan int
    for i := 0; i < p; i++ {
        chs[i] = make(chan int)
    }
    // players in action
    for i := 1; i < p; i++ {
        go player(i, chs[i-1], chs[i])
    }
    go player(p, chs[p-1], chs[0])
    
    fmt.Println("Start the match")
    chs[0] <- 1
    time.Sleep(10 * time.Second)
    
    <-chs[0]
    fmt.Println("Stop!")
}

func player( name int, ball, pass chan int) {
    for {
        touch := <- ball
        fmt.Printf("Player %d: %d\n", name, touch)
        time.Sleep(500*time.Millisecond)
        touch++
        pass <- touch
    }
}

Attenzione però, perchè stranamente il programma non termina subito quando la funzione principale chiede di ricevere il numero da un canale. Quello che accade è che comunque la palla compie un giro fino ad arrivare al canale in cui attende la main().
Il motivo di questo comportamento a mio parere è che nel momento in cui desidereremo interrompere il gioco prelevando un numero da uno dei canali, due goroutine sono in competizione: quella della funzione principale e quella a cui il giocatore precedente vorrebbe inviare il numero, ed in questa competizione vince la prima volta la goroutine del giocatore, e la seconda quella della funzione principale.
In altre parole, la palla va al giocatore successivo e non all’arbitro che comunque avrà successo al passaggio successivo.
Su questo punto sarebbe interessante conoscere la vostra interpretazione. La soluzione che propongo io è quella di creare un canale a parte in cui da main() si spedisce un segnale di interruzione. La funzione player() va modificata con un istruzione select che per prima cosa tenta di ricevere dal canale di interruzione partita, altrimenti spedisce il numero di tocco al giocatore vicino:

// ping pong test
package main

import (
    "fmt"
    "time"
)

const p = 3

func main() {
    // creo p canali
    var chs [p]chan int
    stopsignal := make(chan int)
    for i := 0; i < p; i++ {
        chs[i] = make(chan int)
    }
    // players in action
    for i := 1; i < p; i++ {
        go player(i, chs[i-1], chs[i], stopsignal)
    }
    go player(p, chs[p-1], chs[0], stopsignal)

    fmt.Println("Start the match")
    chs[0] <- 1
    time.Sleep(10 * time.Second)
    fmt.Println("Stop!")
    stopsignal <- 1
    time.Sleep(3 * time.Second)
}

func player(name int, ball, pass, stop chan int) {
    for {
        touch := <-ball
        fmt.Printf("Player %d: %d\n", name, touch)
        time.Sleep(500 * time.Millisecond)
        touch++
        select {
            case <- stop:
                stop <- touch
            default:
                pass <- touch
        }
    }
}

Conclusioni

Ma ci devono sempre essere le conclusioni? Io vorrei continuare a giocare per esempio ma vi lancio volentieri la palla…
Alla prossima.
R.

A proposito di Fibonacci…

Sommario

La semplice funzione per generare la serie di Fibonacci può essere scritta in Go sfruttando le closure.

Fibonacci

Ne abbiamo parlato nel post precedente, ma la sequenza di Fibonacci ha ancora qualcosa da farci scoprire, almeno dal punto di vista del linguaggio di programmazione Go.

Avevano scritto la funzione che calcola l’ennesimo numero della serie a partire dalla definizione della serie: il numero successivo è la somma dei due numeri precedenti. Il codice è quindi il seguente:

package main

// ennesimo numero della serie di Fibonacci
func fibonacci(n int) int {
    var a, b int = 0, 1
    for i := 0; i < n-1; i++ {
        a, b = b, a+b
    }
    return a
}

func main() {
    for i := 1; i < 10; i++ {
        print(fibonacci(i), " ")
    }
    println()
}

Funzioni di prima classe

Quando le funzioni sono tipi di prima classe possono essere trattate come valori, dunque possono essere assegnate a variabili e restituite da una funzione.
Questa prerogativa dei linguaggi funzionali è presente in Lua ed anche in Go (quello che diremo vale indifferentemente per i due linguaggi). Per fare un esempio in Go, assegnamo una funzione ad una variabile per il calcolo della somma degli argomenti interi (ricordo che per provare il codice possiamo utilizzare il comodo servizio web Playground:

package main

func main() {
    add := func(a, b int) int {
        return a + b
    }
    println(add(4,5))
}

Assegnare una funzione ad una variabile significa creare una funzione anonima (senza nome) ma, rispetto alla definizione diretta, cambia solamente la semantica/sintassi del linguaggio ma non il risultato che è esattamente equivalente.

Un esempio con una funzione factory è il seguente dove rispetto ad un simbolo passato come argomento, una funzione restituisce la funzione dell’operazione corrispondente:

package main

func operation(op string) func(int, int) int {
    switch op {
    case "+":
        return func(a, b int) int {
            return a + b
        }
    case "-":
        return func(a, b int) int {
            return a - b
        }
    case "*":
        return func(a, b int) int {
            return a * b
        }
    case "/":
        return func(a, b int) int {
            return a / b
        }
    default:
        return nil
    }
}

func main() {
    add := operation("+")
    println(add(4, 5))
    molt := operation("*")
    println(molt(4, 5))
}

Closure

Quando una funzione di prima classe ha accesso alle variabili locali, le variabili che appartengono allo stesso scopo della funzione, viene chiamata closure.

Tornando a Fibonacci, poiché sono necessari due valori iniziali di innesco della serie, possiamo esprimerli con variabili locali di una closure:

package main

// fibonacci is a function that returns
// a function that returns an int
func fibonacci() func() int {
    a, b := 1, 0
    return func() int {
        a, b = b, a + b
        return a
    }
}

func main() {
    f := fibonacci()
    for i := 0; i < 10; i++ {
        println(f())
    }
}

Le variabili a e b interne alla funzione fibonacci() sono variabili che possono essere lette e scritte dalla funzione anonima, verificando il concetto di funzione closure.
A questo punto possiamo anche creare funzioni di Fibonacci a piacere definendo di volta in volta i primi due numeri della sequenza:

package main

// fibonacci is a function that returns
// a function that returns an int
func fibonacci(n1, n2 int) func() int {
    return func() int {
        n1, n2 = n2, n1 + n2
        return n1
    }
}

func main() {
    f1 := fibonacci(1, 0)
    for i := 0; i < 10; i++ {
        println(f1())
    }
    println()
    f2 := fibonacci(50, 17)
    for i := 0; i < 10; i++ {
        println(f2())
    }
}

Ed in Lua?

-- Lua version : - )
 
-- fibonacci is a function that returns
-- a function that returns an int
function fibonacci(n1, n2)
    return function()
        n1, n2 = n2, n1 + n2
        return n1
        end
end
 
f1 = fibonacci(1, 0)
for i=1,10 do
    print(f1())
end

print()
f2 = fibonacci(50, 17)
for i=1,10 do
    print(f2())
end

Insomma possiamo dire che i progettisti del Go hanno studiato in modo approfondito anche Lua e Python. O no? :-)

Alla prossima…
Un saluto.
R.

Iscriviti

Ricevi al tuo indirizzo email tutti i nuovi post del sito.

Unisciti agli altri 71 follower