Goroutine 101

by Daniele Maccioni

https://github.com/GendoIkari/golab-2015

La realtà è intrinsecamente parallela, multicore, multiutente.

 

Risolvere problemi simultanei su macchine "seriali" è complicato, controintuitivo.

PROBLEMA:

L'applicazione tipica oggi: webapp.

 

Milioni di utenti in contemporanea, distribuita su multipli computer, servizi, infrastrutture, per essere presente su device di ogni tipo.

PROBLEMA:

Gli strumenti "classici" che abbiamo sono nati in un ambiente più simile ad un Commodore 64 che ad una webapp.

Go

  • Concurrency al centro del design
  • Applicazioni moderne (servers, services, webapp...)
  • Performance elevate sfruttando anche hardware multicore

Concurrency?

Concurrency != Parallelism

Concurrency

  • Pattern per comporre cose (funzioni) eseguite in modo indipendente.
  • Prospettiva di design.
  • Come concepire la struttura del programma per gestire esecuzioni simultanee e concorrenti.

 

Parallelism

  • L'atto concreto di eseguire cose (funzioni) in modo indipendente.
  • La CPU è multicore?
  • Il codice viene effettivamente eseguito in modo parallelo?

Tre strumenti

  • goroutine
  • channel
  • select

Goroutine

package main

func doCalc1() {
	// 2 seconds spent here...
}

func doCalc2() {
	// 2 seconds spent here...
}

func doCalc3() {
	// 2 seconds spent here...
}

func main() {
	// ...
	// insert long program here...
	// ...

	// We are executing three functions serially, like usual.
	// Time = 6 seconds.

	doCalc1()
	doCalc2()
	doCalc3()

	// ...
	// insert rest of the program here...
	// ...
}

doCalc1()

doCalc2()

doCalc3()

2s

2s

2s

t

package main

func doCalc1() {
	// 2 seconds spent here
}

func doCalc2() {
	// 2 seconds spent here
}

func doCalc3() {
	// 2 seconds spent here
}

func main() {
	// ...
	// insert long program here...
	// ...

	// We are executing three functions serially, like usual.
	// Time = 6 seconds.
	go doCalc1()
	go doCalc2()
	go doCalc3()

	// ...
	// insert rest of the program here...
	// ...
}

go function()

  • keyword "go" marca le funzioni da eseguire come goroutine
  • l'esecuzione è indipendente
  • marker di "alto livello", l'implementazione è un dettaglio della runtime di go

Cosa sono

  • funzioni eseguite in modo indipendente
  • lightweight micro-thread
  • veloci
  • low overhead
  • stack efficiente
  • usabili nell'ordine delle migliaia e migliaia

... e il risultato?

doCalc1()

doCalc2()

doCalc3()

2s

2s

2s

t

(single core)

func getData() {
	// 2 seconds of network delay
}

func doSlimCalc() {
	// 2 seconds spent here
}

func doFatCalc() {
	// 4 seconds spent here
}

Introduciamo un caso un po' più reale.

  • una richiesta su network (con i suoi tempi)
  • un calcolo cpu-intensive più veloce
  • un calcolo cpu-intensive più lungo

getData()

doSlimCalc()

doFatCalc()

~0s

1s

2s

t

(single core)

~0s

2s

1s

2s

doSlimCalc()

getData()

doFatCalc()

getData()

doSlimCalc()

doFatCalc()

~0s

1s

2s

t

(multi core)

~0s

2s

1s

2s

doSlimCalc()

getData()

doFatCalc()

doSlimCalc()

doFatCalc()

Channel

Canali per mandare e ricevere dati, a prova di concurrency.

ch1 := make(chan int)

ch1 <- 1

<-ch1

  • one-to-one
  • mono o bidirezionali
  • concurrency safe
  • tipizzati
  • first-class citizen
package main

import (
	"fmt"
	"time"
)

func main() {

	ch1 := make(chan string)

	go func() {
		ch1 <- "Hello"
		ch1 <- "World!"
	}()

	go func() {
		for {
			fmt.Println(<-ch1)
		}
	}()

	time.Sleep(1 * time.Second)
}
Hello
World!

"Don't communicate by sharing memory.

Share memory by communicating."

THIS!

NOT THIS!

THIS!

NOT THIS!

sharedInput := 0
sharedOutput := 0

go func() {
    for {
	// ...
	sharedInput = rand.Int()
	// ...
    }
}()

go func() {
    for {
	// ...
	sharedOutput = sharedInput * 2
	// ...
    }
}()
inputs := make(chan int)
outputs := make(chan int)

go func() {
    for {
	// ...
	inputs <- rand.Int()
	// ...
    }
}()

go func() {
    for {
	// ...
	input := <-inputs
	outputs <- input * 2
	// ...
    }
}()

Lettura e scrittura su un canale è bloccante, se non è bufferizzato.

 

I channel bufferizzato possono contenere dati prima di diventare bloccanti.

 

ch := make(chan int, 10)

channel := make(chan int, 10)

go func() {
    for i := 0; i < 10; i++ {
	channel <- i
	time.Sleep(100 * time.Millisecond)
    }
}()

time.Sleep(500 * time.Millisecond)

for i := 0; i < 5; i++ {
    <-channel
}

time.Sleep(500 * time.Millisecond)

for i := 0; i < 5; i++ {
    <-channel
}
  • in scrittura è bloccante solo se il buffer è pieno
  • in lettura è bloccante solo se il buffer è vuoto
real	0m1.001s
user	0m0.000s
sys	0m0.000s
func addData() {

    data := make(chan int)

    go func() {
        // get data from internet
        // ...
        data <- downloadNumber()
    }

    go func() {
        // get data from user
        // ...
        data <- inputNumber()
    }

    x := <-data
    y := <-data

    return x + y
}
  • in scrittura è bloccante fino ad una lettura
  • in lettura è bloccante fino ad una scrittura
  • di fatto è un modo per fare il sync
func addData() {

    data := make(chan int)

    go func() {
        for i := 0; i < 100; i++ {
	    data <- i
	}
	close(data)
    }()

    sum := 0

    for i := range data {
	sum += i
    }

    return sum
}

I channel si possono chiudere per segnalare la fine della comunicazione. 

La range su un channel estrae dati fino alla sua chiusura.

Select

func producerSlow(output chan string) {
	for {
		output <- "Message!"
		time.Sleep(1 * time.Second)
	}
}

func producerFast(output chan int) {
	for {
		output <- rand.Int()
		time.Sleep(500 * time.Millisecond)
	}
}
func main() {
	ch1 := make(chan string)
	ch2 := make(chan int)

	go producerSlow(ch1)
	go producerFast(ch2)

	for {
		fmt.Println(<-ch1)
		fmt.Println(<-ch2)
	}
}
Message!
5577006791947779410
Message!
8674665223082153551
Message!
6129484611666145821
Message!
4037200794235010051
func main() {
	ch1 := make(chan string)
	ch2 := make(chan int)

	go producerSlow(ch1)
	go producerFast(ch2)

	for {
		select {
		case msg := <-ch1:
			fmt.Println(msg)
		case num := <-ch2:
			fmt.Println(num)
		}
	}
}
Message!
3916589616287113937
6334824724549167320
Message!
605394647632969758
1443635317331776148
  • analoga alla select dei socket
  • bloccante, fino a che uno dei channel non è pronto
  • switch a prova di concurrency
  • gestisce la composizione di canali
func main() {
	ch1 := make(chan string)
	ch2 := make(chan int)

	go producerSlow(ch1)
	go producerFast(ch2)

	for {
		select {
		case msg := <-ch1:
			fmt.Println(msg)
		case num := <-ch2:
			fmt.Println(num)
		case <-time.After(250 * time.Millisecond):
			fmt.Println("Timeout!")
		}
	}
}
Message!
5577006791947779410
Timeout!
Timeout!
8674665223082153551
Timeout!
Message!
6129484611666145821

func After(d Duration) <-chan Time

Default

La select ha la possibilità di avere un caso di default, eseguito nel caso tutti gli altri siano bloccati.

La presenza di un default rende di fatto la select non bloccante.

select {

case <- channel1: 
    // ...

case <- channel2: 
    // ...

case <- channel3: 
    // ...

case default:
    // ...    

}
func main() {
	ch1 := make(chan string)
	ch2 := make(chan int)

	go producerSlow(ch1)
	go producerFast(ch2)

	for {
		select {
		case msg := <-ch1:
			fmt.Println(msg)
		case num := <-ch2:
			fmt.Println(num)
		default:
			fmt.Println("Missing data!")
			time.Sleep(250 * time.Millisecond)
		}
	}
}
Missing data!
5577006791947779410
Message!
Missing data!
Missing data!
8674665223082153551
Missing data!
Missing data!
Message!
6129484611666145821
Missing data!
Missing data!
4037200794235010051

Conclusioni

Go offre una prospettiva sulla concurrency solida, efficace, semplice.

 

Goroutine, channel e select sono primitive di linguaggio facili da usare e da capire, utili a costruire strumenti più sofisticati.

 

L'approccio di Go rende veramente accessibile il design di applicazioni asincrone e parallele a tutti.

Domande?

Goroutine 101

By Gendo Ikari

Goroutine 101

GoLab 2015

  • 75