4. • Individuals populate this world
• They act sometimes independent of each other, sometimes
dependent, sometimes together
• Communication and signals enable their coexistence
It's a natural principle Frank Müller
13. • Computer architectures are changing
• Growth via CPUs, cores and hyper-threads
• Manual usage via threads is complex and error-prone
• Concurrent runtime environments enable fine-grained usage
One motivation is the hardware Frank Müller
14. Distribution of computing power Frank Müller
Process Process Process Process
Process Process Process Process
Runtime Envirunment
Core Core Core Core
Process
Process
Process
Process
17. • Encapsulation of the state in the process
• Communication via messages
• Sequential processing
• Atomic state changes
• OOP in the real sense
Structure is more important motivation Frank Müller
18. ❞ –Rob Pike
Parallelis
m
Programming as the simultaneous execution
of (possibly related) computations
.
Concurrenc
y
Programming as the composition of
independently executing processes.
19. • Actor Model
‣ 1973
‣ Carl Hewitt, Peter Bishop und Richard Steiger
• Communicating Sequential Processes
‣ 1978
‣ Tony Hoare
Ideas long known Frank Müller
20. • Concurrent processes communicate via one or more channels
• Processes, unlike channels, are anonymous
• Data is not sent until the receiver is ready to receive it
• Incoming data is processed sequentially
Communicating Sequential Processes Frank Müller
24. Processes provide services Frank Müller
Service
Provider
Client
Client Client
Client
Active
Waiting
Waiting
Waiting
25. Processes manage ressources Frank Müller
Client
Client
Client
Manager Resource B
Resource A
Resource C
Active
Waiting
Waiting
Read / Write
26. Processes manage parallel requests Frank Müller
Worker
Worker
Worker
Client
Client
Client
Master
Request A
Request B
Request C
Request A
Request B
Request C
Reply
28. Processes monitor each other Frank Müller
Supervisor
Process A Process B
Process B'
Starts A Starts B
Monitors A Monitors B
Monitors B'
Restarts B'
29. Processes support powerful ETL Frank Müller
Sender
Sender
Sender
Transformator(s) Loader
Extractor Receiver
Receiver
Receiver
Transformator(s)
Transformator(s)
Transformed
Data
Transformed
Data
Transformed
Data
Raw
Data
Raw
Data
Raw
Data
31. • Development started 2007 by Google
• Designed by Rob Pike, Ken Thompson, and Robert Griesemer
• Initial release 2012
• Looks imperative, but is multi-paradigm
• Types with methods, interfaces, function types, concurrency
• Concurrency realized by goroutines and channels
Google Go Frank Müller
32. • Goroutines run lightweight in a thread pool
• Functions spawned with the keyword go
• Large simultaneous number possible
• Channels are typed and run synchronous or buffered
• They are used for communication and synchronization
• select statements allow parallel processing of multiple channels
• Use in for loops enables continuous processing
Concurrency in Go Frank Müller
34. // processMyData processes the data in the background.
func processMyData(data Data) { ... }
// startProcessing pre-processes the data and starts the goroutine.
func startProcessing() {
var data Data
data = ...
// Spawn goroutine.
go processMyData(data)
// Do something else.
...
}
Simple in own function Frank Müller
35. // startProcessing pre-processes the data and starts the goroutine to process
// it in the background.
func startProcessing() {
var data Data
data = ...
// Spawn goroutine, function uses outer data.
go func() {
...
}()
// Do something else.
...
}
Simple in embedded function Frank Müller
36. // startProcessing pre-processes the data and starts the goroutine to process
// a copy in the background.
func startProcessing() {
var data Data
data = ...
// Spawn goroutine with a data copy, same name is no problem.
go func(data Data) {
...
}(data)
// Do something else using the original data.
...
}
Embedded function with data copy Frank Müller
37. // process pre-processes the data, starts the goroutine and waits until it's
// done.
func process() {
data := ...
var wg sync.WaitGroup // sync.WaitGroup allows counting of activities.
wg.Add(1) // In example here we wait for only one processing.
go processData(&wg, data)
...
wg.Wait() // Wait until it's done.
}
Waiting for processing of data Frank Müller
38. // processData processes the passed data and signals its ending via
// the sync.WaitGroup.
func processData(wg *sync.WaitGroup, data Data) {
// Deferred function call tells wait group that one processing is done.
defer wg.Done()
// Process data data.
...
}
Tell waiter that work is done Frank Müller
39. // processDatas starts the goroutines to process the individual datas and waits
// until all are done.
func processDatas(datas []Data) {
var wg sync.WaitGroup
for _, data := range datas {
// Add one per each data to process.
wg.Add(1)
// Spawn processing like in last example.
go processData(&wg, data)
}
wg.Wait() // Wait until they are done.
}
Start a number of background jobs Frank Müller
41. // processDatas processes all datas it receives via the data channel.
// Loop ends when the channel is closed.
func processDatas(dataChan <-chan Data) {
for data := range dataChan {
// Process the individual data sequentially.
...
}
}
Process all datas received from channel Frank Müller
42. // Create data channel.
dataChan := make(chan Data)
// Spawn processor with data channel.
go processDatas(dataChan)
// Send datas.
dataChan <- dataA
dataChan <- dataB
dataChan <- dataC
// Close channel.
close(dataChan)
Use the data processor Frank Müller
43. // processAtoB processes all A datas it receives via the in channel. Results of
// type B are written to the out channel. That will be closed if the function
// ends working a.k.a. the in channel has been closed.
func processAtoB(inChan <-chan A, outChan chan<- B) {
defer close(outChan)
for a := range inChan {
b := ...
outChan <- b
}
}
// processBtoC works similar to processAtoB, only with B and C datas.
func processBtoC(inChan <-chan B, outChan chan<- C) {
...
}
Piping Frank Müller
44. // Create buffered channels.
aChan := make(chan A, 5)
bChan := make(chan B, 5)
cChan := make(chan C, 5)
// Spawn processors.
go processAtoB(aChan, bChan)
go processBtoC(bChan, cChan)
go processCs(cChan)
// Write A data into A channel, then close.
...
close(aChan)
Use the piping Frank Müller
46. • Structure with data and channels
• Function New() as constructor
• Method loop() or backend() for loop with select statement
• Public methods to access the instance
• Requests with return values need explicit channel
• Multiple return values need helper types
Often found pattern Frank Müller
47. // MyService simply provides a string and an integer field and allows to add
// them if possible.
type MyService struct {
a string
b int
// Many channels needed this way.
setAChan chan string
getAChan chan chan string
setBChan chan int
getBChan chan chan int
addChan chan addResp
}
Structure Frank Müller
48. // New create a new instance of MyService. The backend goroutine is controlled
// by the given context.
func New(ctx context.Context) *MyService {
ms := &MyService{
setAChan: make(chan string),
getAChan: make(chan chan string),
setBChan: make(chan int),
getBChan: make(chan chan int),
addChan: make(chan addReq),
}
go ms.backend(ctx)
return ms
}
Constructor Frank Müller
49. // SetA simply sends the data via the channel.
func (ms *MyService) SetA(a string) {
ms.setAChan <- a
}
// GetA sends a buffered channel and receives the result via it.
func (ms *MyService) GetA() string {
// Buffered to allow backend continue working.
respChan := make(chan string, 1)
ms.getAChan <- respChan
return <-respChan
}
Setter and getter Frank Müller
50. // addResp is a private transporter for the sum and a possible error.
type addResp struct {
sum int
err error
}
// Add sends a buffered channel for a transporter.
func (ms *MyService) Add() (int, error) {
respChan := make(chan addResp, 1)
ms.addChan <- retChan
resp := <-retChan
if resp.err != nil { return 0, resp.err }
return resp.sum, nil
}
Sometimes even more effort needed Frank Müller
51. // backend runs as goroutine and serializes the operations.
func (ms *MyService) backend(ctx context.Context) {
// Endless loop with for.
for {
// Select to switch between the channels.
select {
case <-ctx.Done():
// Terminate backend.
return
case ...:
...
}
}
}
Backend structure Frank Müller
52. select {
...
case respChan := <-ms.getBChan:
respChan <- ms.b
case respChan := <-ms.addChan:
var resp addResp
i, err := strconv.Atoi(ms.a)
if err != nil {
resp.err = err
} else {
resp.sum = i + ms.b
}
respChan <- resp
}
Other cases Frank Müller
53. • Much extra effort with typed channels and helper types
• Example here does not care if context is cancelled
• Public methods and business logic are separated
• Without private helpers the select statement may grow too much
• But thankfully there's a more simple way to do it
Summary Frank Müller
55. // Actor only needs two extra fields.
type Actor struct {
ctx context.Context
actChan chan func()
// Here own data.
...
}
func New(ctx context.Context) *Actor {
act := &Actor{ ... }
go backend()
return act
}
Structure and constructor Frank Müller
56. // backend is only executing the received functions.
func (act *Actor) backend() {
for {
select {
case <-act.ctx.Done():
return
case action := <-act.actChan:
// Execute received function.
action()
}
}
}
Backend Frank Müller
57. // do always keeps an eye on the context when sending.
func (act *Actor) do(action func()) error {
select {
case <-act.ctx.Done():
if ctx.Err() != nil {
return ctx.Err()
}
return errors.New("actor has been stopped")
case act.actChan <- action:
return nil
}
}
Safe sending of a function Frank Müller
58. // Add works like the Add() from example before.
func (act *Actor) Add() (i int, err error) {
// Send logic to the backend.
if aerr := act.do(func() {
fi, ferr := strconv.Atoi(act.a)
if ferr != nil {
err = ferr
return
}
i = fi + act.b
}); aerr != nil {
// Looks like the actor already has been stopped.
return 0, aerr
}
}
Business logic Frank Müller
59. • Less code
• Actor can be implemented in one package and always reused
• Additional buffered channel and doAsync() support pure setters
and callers without return values
• do() and doAsync() also could get optional timeout for fault
tolerant behavior
Summary Frank Müller
61. // Supervisor helps monitoring and restarting concurrent functions.
type Supervisor struct {
mu sync.Mutex
workers map[string]func() error
spawnChan chan string
}
// New starts the supervisor in the background.
func New(ctx context.Context) *Supervisor {
s := &Structure{ ... }
go s.backend(ctx)
return s
}
Structure and constructor Frank Müller
62. // Spawn tells backend to spawn the given worker.
func (s *Supervisor) spawn(id string, worker func() error) error {
s.mu.Lock()
defer s.mu.Unlock()
_, ok := s.workers[id]
if ok {
return errors.New("double worker ID")
}
s.workers[id] = worker
s.spawnChan <- id
return nil
}
Start a worker Frank Müller
63. // wrap takes care for errors of the worker. In case of an error it notifies
// the backend to re-spawn.
func (s *Supervisor) wrap(id string) {
worker := s.workers[id]
if err := worker(); err != nil {
// Log the error and re-spawn the worker.
log.Printf("worker %q terminated with error: %v", id, err)
s.spawnChan <- id
return
}
// Delete successful terminated worker.
s.mu.Lock()
delete(s.workers, id)
s.mu.Unlock()
}
Wrapper to check if worker error Frank Müller
64. // backend wraps workers and spawns them.
func (s *Supervisor) backend(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
case id := <-s.spawnChan:
go s.wrap(id)
}
}
}
Backend Frank Müller
66. • Channels work synchron or buffered
• Working reader could lead to blocking writes, buffers may be
filled
• Most times just waiting, but overload resource may lead to
instability
• If needed check for parallelized pre-processing steps, e.g. by
caller, and keep the need for serialization small
• Alternatively assistent workers for the backend may help
Blocked channels Frank Müller
67. • Avoid overlapping read and write access with external
modification
• E.g. use IncrBy() instead of Get(), local addition, and Set()
• Alternatively read value together with timed handle for update
• Other modifiers get an error during this time
• An unused handle must be released again
Race conditions Frank Müller
68. • Concurrent updates of coherent data may lead to invalid states
• Avoid too fine granular access
• Assure changes of all related data en bloc
Non-atomic updates Frank Müller
70. • Power of concurrency seems to be complex
• Possibilities are manifold
• As is often the case, only a few patterns make up almost all use
cases
• Own or 3rd party packages reduce work here
• Design of elastic software requires a more natural rethink
Final summary Frank Müller
71. Thanks a lot and
have a nice
evening
Image Sources
123RF
Pexels
iStockphoto
Own photos