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 n 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