Slide 1

Slide 1 text

Understanding channels @kavya719

Slide 2

Slide 2 text

kavya

Slide 3

Slide 3 text

inner workings of channels

Slide 4

Slide 4 text

goroutines to execute tasks independently, potentially in parallel. channels for communication, synchronization between goroutines. concurrency features

Slide 5

Slide 5 text

func main() { tasks := getTasks() // Process each task. for _, task := range tasks { process(task) }
 
 ... } hellaTasks

Slide 6

Slide 6 text

task queue func worker(ch) { for {
 // Get a task. task := <-taskCh
 process(task) } } func main() { // Buffered channel. ch := make(chan Task, 3) // Run fixed number of workers. for i := 0; i < numWorkers; i++ { go worker(ch) } // Send tasks to workers. hellaTasks := getTasks() for _, task := range hellaTasks { taskCh <- task } ... }

Slide 7

Slide 7 text

task queue func worker(ch) { for {
 // Receive task. task := <-taskCh
 process(task) } } func main() { // Buffered channel. ch := make(chan Task, 3) // Run fixed number of workers. for i := 0; i < numWorkers; i++ { go worker(ch) } // Send tasks to workers. hellaTasks := getTasks() for _, task := range hellaTasks { taskCh <- task } ... }

Slide 8

Slide 8 text

task queue func worker(ch) { for {
 // Receive task. task := <-taskCh
 process(task) } } func main() { // Buffered channel. ch := make(chan Task, 3) // Run fixed number of workers. for i := 0; i < numWorkers; i++ { go worker(ch) } // Send tasks to workers. hellaTasks := getTasks() for _, task := range hellaTasks { taskCh <- task } ... }

Slide 9

Slide 9 text

goroutine-safe. store and pass values between goroutines. provide FIFO semantics. can cause goroutines to block and unblock. channels are inherently interesting

Slide 10

Slide 10 text

making channels the hchan struct stepping back: design considerations sends and receives goroutine scheduling

Slide 11

Slide 11 text

making channels

Slide 12

Slide 12 text

unbuffered channel ch := make(chan int) buffered channel ch := make(chan Task, 3) make chan

Slide 13

Slide 13 text

goroutine-safe stores up to capacity elements,
 and provides FIFO semantics sends values between goroutines can cause them to block, unblock buffered channel ch := make(chan Task, 3)

Slide 14

Slide 14 text

buf lock ... recvx circular queue mutex send index receive index sendx hchan buffered channel ch := make(chan Task, 3)

Slide 15

Slide 15 text

buf lock ... recvx circular queue mutex send index receive index sendx hchan 0 0 2 1 0 empty buffered channel ch := make(chan Task, 3)

Slide 16

Slide 16 text

buf lock ... recvx circular queue mutex send index receive index sendx hchan 0 1 buffered channel ch := make(chan Task, 3) an enqueue 2 1 0

Slide 17

Slide 17 text

buf lock ... recvx circular queue mutex send index receive index sendx hchan 0 0 buffered channel ch := make(chan Task, 3) two more; so, full 2 1 0

Slide 18

Slide 18 text

buf lock ... recvx circular queue mutex send index receive index hchan sendx 1 0 buffered channel ch := make(chan Task, 3) a dequeue 2 1 0

Slide 19

Slide 19 text

make chan heap stack high addr low addr program memory buffered channel ch := make(chan Task, 3) allocates an hchan struct on the heap. initializes it. returns a pointer to it.

Slide 20

Slide 20 text

make chan heap allocates an hchan struct on the heap. initializes it. returns a pointer to it. ch buffered channel ch := make(chan Task, 3) This is why we can pass channels between functions, don’t need to pass 
 pointers to channels.

Slide 21

Slide 21 text

sends and receives

Slide 22

Slide 22 text

task queue func main() { ... for _, task := range tasks { taskCh <- task } ... } func worker() { for { task := <-taskCh
 process(task) } } G2 G1

Slide 23

Slide 23 text

ch <- task0 buf ... lock G1

Slide 24

Slide 24 text

ch <- task0 G1 buf ... lock 1. acquire

Slide 25

Slide 25 text

ch <- task0 buf ... lock 2. enqueue task0 copy G1

Slide 26

Slide 26 text

ch <- task0 buf ... lock 3. release task0 copy G1

Slide 27

Slide 27 text

t := <-ch G2 buf ... lock task0 copy

Slide 28

Slide 28 text

buf ... lock 1. acquire task0 copy t := <-ch G2

Slide 29

Slide 29 text

buf ... lock 2. dequeue task0 copy’ t := <-ch G2

Slide 30

Slide 30 text

buf ... lock 3. release task0 copy’ t := <-ch G2

Slide 31

Slide 31 text

no shared memory
 (except hchan) copies

Slide 32

Slide 32 text

“Do not communicate by sharing memory; 
 instead, share memory by communicating.” no shared memory
 (except hchan) copies

Slide 33

Slide 33 text

buf ... lock G1 G2

Slide 34

Slide 34 text

buf ... lock ch <- task1 G1 G2

Slide 35

Slide 35 text

buf ... lock ch <- task1 ch <- task2 G1 G2

Slide 36

Slide 36 text

buf ... lock ch <- task1 ch <- task2 ch <- task3 G1 G2

Slide 37

Slide 37 text

buf ... lock ch <- task1 ch <- task2 ch <- task3 ch <- task4 G1 G2

Slide 38

Slide 38 text

buf ... lock ch <- task1 ch <- task2 ch <- task3 ch <- task4 ruh-roh, channel is full! G1’s execution is paused, resumed after a receive. G1 G2

Slide 39

Slide 39 text

buf ... lock ch <- task1 ch <- task2 ch <- task3 ch <- task4 ? ruh-roh, channel is full! G1’s execution is paused, resumed after a receive. G1 G2

Slide 40

Slide 40 text

goroutines are user-space threads. created and managed by the Go runtime, not the OS. lightweight compared to OS threads. the runtime scheduler schedules them onto OS threads. g5 g1 g4 g3 OS thread2 M:N scheduling OS thread1 g1 g2 g6 g2 interlude: the runtime scheduler

Slide 41

Slide 41 text

Go’s M:N scheduling is described using three structures: M: OS thread G: goroutine P: context for scheduling. M

Slide 42

Slide 42 text

M G Go’s M:N scheduling is described using three structures: M: OS thread G: goroutine P: context for scheduling.

Slide 43

Slide 43 text

M P G Go’s M:N scheduling is described using three structures: M: OS thread G: goroutine P: context for scheduling.

Slide 44

Slide 44 text

Ps hold the runqueues. In order to run goroutines (G),
 a thread (M) must hold a context (P). M P runQ G G G runnable Go’s M:N scheduling is described using three structures: M: OS thread G: goroutine P: context for scheduling. }

Slide 45

Slide 45 text

Ps hold the runqueues. In order to run goroutines (G),
 a thread (M) must hold a context (P). M P runQ G G G runnable current G running Go’s M:N scheduling is described using three structures: M: OS thread G: goroutine P: context for scheduling. }

Slide 46

Slide 46 text

pausing goroutines ch <- task4 send on a full channel }

Slide 47

Slide 47 text

ch <- task4 calls into the scheduler gopark M P G1 G G runQ runnable current G running } G1

Slide 48

Slide 48 text

M P G1 G G runnable runQ current G waiting ch <- task4 sets G1 to waiting calls into the scheduler gopark } G1

Slide 49

Slide 49 text

ch <- task4 sets G1 to waiting calls into the scheduler removes association between G1, M gopark M P G G G1 waiting runQ runnable } G1

Slide 50

Slide 50 text

G1 waiting M P G G runQ runnable current G ch <- task4 schedules a runnable G 
 from the runqueue sets G1 to waiting calls into the scheduler removes association between G1, M “returns” 
 to a different G } G1 gopark

Slide 51

Slide 51 text

This is neat. G1 is blocked as needed, but not the OS thread.

Slide 52

Slide 52 text

This is neat. G1 is blocked as needed, but not the OS thread. …great, but how do we resume the blocked goroutine? after a channel receive, and the channel is no longer full

Slide 53

Slide 53 text

the hchan struct stores waiting senders, receivers as well. ... hchan sendq recvq waiting senders waiting receivers buf lock G elem sudog ... waiting G elem to send/ recv resuming goroutines represents a G in a waiting list stored as a sudog

Slide 54

Slide 54 text

G elem sudog ... G1 task4 ch <- task4 G1 creates a sudog for itself, puts it in the sendq happens before calling into the scheduler.

Slide 55

Slide 55 text

... hchan sendq recvq G elem sudog ... G1 task4 ch <- task4 G1 creates a sudog for itself, puts it in the sendq happens before calling into the scheduler. receiver uses it to resume G1.

Slide 56

Slide 56 text

... hchan sendq recvq G elem sudog ... G1 task4 ch <- task4 G1 creates a sudog for itself, puts it in the sendq happens before calling into the scheduler. receiver uses it to resume G1.

Slide 57

Slide 57 text

sendq ... g elem ... buf lock task4 task1 G1 t := <-ch full buffer waiting sender { { G2 waiting to send task4 ||

Slide 58

Slide 58 text

sendq ... g elem ... buf lock task4 t := <-ch G1 task1 dequeue G2

Slide 59

Slide 59 text

t := <-ch task1 sendq ... g elem ... buf lock task4 G1 pops off sudog G2

Slide 60

Slide 60 text

task4 task1 sendq ... g elem ... buf lock G1 t := <-ch enqueues the sudog’s elem: G2

Slide 61

Slide 61 text

need to set G1 to runnable task1 task4 sendq ... g elem ... buf lock G1 t := <-ch G2

Slide 62

Slide 62 text

t := <-ch calls into the scheduler G1 waiting M P G2 G runQ current G G2 goready
 (G1)

Slide 63

Slide 63 text

goready
 (G1) t := <-ch sets G1 to runnable calls into the scheduler G1 runnable M P G2 G runQ current G G2

Slide 64

Slide 64 text

t := <-ch sets G1 to runnable calls into the scheduler puts it on runqueue G1 M P G2 G runQ current G returns to G2 G2 }runnable goready
 (G1)

Slide 65

Slide 65 text

sends and receives when the receiver comes first

Slide 66

Slide 66 text

recvq ... buf lock t := <-ch G2 receive on an empty channel:
 G2’s execution is paused resumed after a send. empty buffer {

Slide 67

Slide 67 text

receive on an empty channel:
 G2’s execution is paused resumed after a send. recvq ... buf lock G2 t := <-ch empty buffer {

Slide 68

Slide 68 text

receive on an empty channel:
 G2’s execution is paused resumed after a send. recvq ... buf lock G2 t := <-ch empty buffer { set up state for resumption, and pause put a sudog in the recvq gopark G2.

Slide 69

Slide 69 text

recvq ... buf lock G elem ... G2 t empty buffer waiting receiver { { G2 t := <-ch set up state for resumption, and pause put a sudog in the recvq gopark G2. waiting to receive to t ||

Slide 70

Slide 70 text

recvq ... buf lock elem ... G t G2 ch <- task G1

Slide 71

Slide 71 text

recvq ... buf lock elem ... G t G2 enqueue task in the buffer, goready(G2) could: ch <- task G1

Slide 72

Slide 72 text

recvq ... buf lock elem ... G t G2 or we can be smarter. ch <- task G1

Slide 73

Slide 73 text

recvq ... buf lock elem ... G t G2 ch <- task G1 or we can be smarter. the memory location for the receive

Slide 74

Slide 74 text

recvq ... buf lock elem ... G t G2 ch <- task G1 G1 writes to t directly. ! the memory location for the receive

Slide 75

Slide 75 text

} per-goroutine stacks heap stack G1 stack G2 stack t only operations in runtime where this happens. G1 writes to G2’s stack! direct send

Slide 76

Slide 76 text

This is clever. On resuming, G2 does not need to acquire channel lock and manipulate the buffer. 
 Also, one fewer memory copy.

Slide 77

Slide 77 text

goroutine-safe hchan mutex store values, pass in FIFO. copying into and out of hchan buffer can cause goroutines to pause and resume. hchan sudog queues calls into the runtime scheduler 
 (gopark, goready) we now understand channels (sorta)…

Slide 78

Slide 78 text

a note (or two)… unbuffered channels
 unbuffered channels always work like the “direct send” case: receiver first —> sender writes to receiver’s stack. sender first —> receiver receives directly from the sudog.
 select (general-case) all channels locked. a sudog is put in the sendq /recvq queues of all channels. channels unlocked, and the select-ing G is paused. CAS operation so there’s one winning case. resuming mirrors the pause sequence.

Slide 79

Slide 79 text

stepping back…

Slide 80

Slide 80 text

simplicity and performance

Slide 81

Slide 81 text

queue with a lock preferred to lock-free implementation: “The performance improvement does not materialize from the air, it comes with code complexity increase.” — dvyokov simplicity

Slide 82

Slide 82 text

calling into the runtime scheduler: OS thread remains unblocked. cross-goroutine stack reads and writes. goroutine wake-up path is lockless, potentially fewer memory copies need to account for memory management: 
 garbage collection, stack-shrinking performance }

Slide 83

Slide 83 text

simplicity and performance astute trade-offs between

Slide 84

Slide 84 text

“The noblest pleasure is the joy of understanding.” - Leonardo da Vinci @kavya719 speakerdeck.com/kavya719/understanding-channels

Slide 85

Slide 85 text

Railgun: CloudFlare’s web proxy We chose to use Go because Railgun is inherently highly concurrent… Railgun makes extensive use of goroutines and channels. Docker: software container platform …Go is geared for distributed computing. It has many built-in features to support concurrency… Doozer: Heroku’s distributed data store Fortunately, Go’s concurrency primitives made the task much easier. Kubernetes: Google’s container orchestration platform Built in concurrency. Building distributed systems in Go is helped tremendously by being able to fan out …

Slide 86

Slide 86 text

unbuffered channels unbuffered channel ch := make(chan int) That’s how unbuffered channels (always) work too.
 If receiver is first: > puts a sudog on the recvq and is paused. > subsequent sender will “send directly”, and resume the receiver. If sender is first: 
 > puts a sudog for itself on the sendq and is paused.
 > receiver then “receives directly” from the sudog and 
 resumes the sender.

Slide 87

Slide 87 text

func worker() { for { select { case task := <-taskCh: process(task) case cmd := <-cmdCh: execute(cmd) } } } var taskCh = make(chan Task, 3) var cmdCh = make(chan Command) selects