Synchronized goroutines (part II)

Channel communication

The most intuitive order relation of send and receiving operations has been introduced during the first part:

Sending to a channel happens before receiving sent value.

Thanks to that we can order operations spread across two goroutines:

var v int

var wg sync.WaitGroup

wg.Add(2)

ch := make(chan int)

go func() {

v = 1

ch <- 1

wg.Done()

}()

go func() {

<-ch

fmt.Println(v)

wg.Done()

}()

wg.Wait()

(definition of main function and imports have been omitted for clarity)

Operations are ordered as follows (x → y means that x happens before y):

v = 1 → ch <- 1 → <-ch → fmt.Println(v)

But besides the one above there is more and this story will be dedicated to channels.

send ↔ receive

There is a complementary rule to the first one. It says that receiving happens before send completes:

send starts → receive → send ends

var v, w int

var wg sync.WaitGroup

wg.Add(2)

ch := make(chan int)

go func() {

v = 1

ch <- 1

fmt.Println(w)

wg.Done()

}()

go func() {

w = 2

<-ch

fmt.Println(v)

wg.Done()

}()

wg.Wait()

Because of the new rule, more operations are ordered:

w = 2 → <-ch → send operation ch <- 1 completes → fmt.Println(w)

It’s possible to solve our initial problem of displaying variable v , ensuring that assignment has been already done:

go func() {

v = 1

<-ch

wg.Done()

}()

go func() {

ch <- 1

fmt.Println(v)

wg.Done()

}()

Now the goroutine which needs to wait till the assignment v = 1 is done, sends value over a channel. Send operations completes after the corresponding receive:

v = 1 → <-ch → ch <- 1 completes → fmt.Println(v)

Channel closing

When channel is closed then receive operations return zero value of channel’s type:

ch := make(chan int)

close(ch)

fmt.Println(<-ch) // prints 0

Closing channel happens before receiving zero value from closed channel

It can be used to solve our initial problem just by replacing send operation with call of built-in close:

go func() {

v = 1

close(ch)

wg.Done()

}()

go func() {

<-ch

fmt.Println(v)

wg.Done()

}()

The order of operations is:

v = 1 → close(ch) → <-ch → fmt.Println(v)

Buffered channels

So far we’ve talked about unbuffered channels. Buffered channels don’t block on send when buffer is not full and do not block on receiving when buffer is not empty:

ch := make(chan int, 1)

ch <- 1

fmt.Println(<-ch)

Code above doesn’t end up in a deadlock even if while sending there is no ready receiver.

For buffered channels all rules covers so far still hold except one which says that receiving happens before send completes. It’s simply because send on buffered channels can finish without ready receiver (if buffer is not full).

The kth receive on a channel with capacity c happens before the (k+c)th send from that channel completes.

Suppose capacity is set to 3. First 3 operations of sending value over a channel can return without accompanying receive statement. But in order to 4th send to complete at least one receive would need to be done.

var v int

var wg sync.WaitGroup

wg.Add(2)

ch := make(chan int, 3)

go func() {

v = 1

<-ch

wg.Done()

}()

go func() {

ch <- 1

ch <- 1

ch <- 1

ch <- 1

fmt.Println(v)

wg.Done()

}()

wg.Wait()

This snippet solves our initial problem using buffered channel.