GO Channels, Waitgroups & Mutexes

This article intends to explain some basic concepts of Go Language: goroutines, channels, waitgroups and mutexes.

At the end of the article there is a simple example of a sync.Once. To deepdive more into Go language you can visit: https://tour.golang.org/.

Let’s start with GOROUTINES

A goroutine is a function that is capable of running concurrently with other functions, it’s a lightweight thread of execution. 

To create a goroutine we use the keyword go followed by a function invocation.  It can also start a goroutine for an anonymous function call. 

The calling code doesn't wait for the goroutine to finish, but just continues running through the rest of the code.

The two function calls are running asynchronously in separate goroutines, waiting for them to finish (for a more robust approach a WaitGroup can be used).

OUTPUT

go run goroutines.go

direct : 0

direct : 1

goroutine : 0

goroutine : 1

going

done

When this program runs, the output of the blocking call is shown first, then the output of the two goroutines. Goroutines run concurrently, but not necessarily in parallel. 

When a goroutine is scheduled to run by calling go func, you’re asking the Go runtime to execute that function as soon as it can.

 But that’s likely not immediately. In fact, if the Go program can use only one processor, it is almost certain that it won’t run immediately.

 Instead, the scheduler will continue executing the outer function until a circumstance arises that causes it to switch to another task.

CHANNELS 

Channels are the pipes that connect concurrent goroutines. Values can be sent into channels from one goroutine and receive those values into another goroutine. 

Channels are typed by the values. Create a new channel with make: 

 messages := make(chan string)

 Send a value into a channel using the channel <- syntax. Here "sending" is sent to the messages channel, from a new goroutine. 

 go func() { messages <- "sending" }() 

The <-channel syntax receives a value from the channel.

 

By default channels are unbuffered, meaning that they will only accept sends (chan <-) if there is a corresponding receive (<- chan) ready to receive the sent value.

Buffered channels accept a limited number of values without a corresponding receiver for those values.

OUTPUT

14 -3 

Using a channel, the main task will wait until the asynchronous task is complete. It blocks until it receives a notification from the worker on the channel. 

When the goroutine completes its work, it will send a value through the channel, which will be read before operating on the numbers array. 

Buffered channels accept a limited number of values without a corresponding receiver for those values. 

Channels can be buffered if the intention is to prevent blocking further execution until a value is eventually read from the channel to free it up.

 Buffered channels are blocked only when the buffer is full.

 Similarly, receiving from a buffered channel is blocked only when the buffer will be empty. 

OUTPUT

1

0

Range Over Channels

This range iterates over each element as it’s received from the queue.

 Because the above channel is closed, the iteration terminates after receiving the 2 elements.

This example also shows that it’s possible to close a non-empty channel but still have the remaining values be received.

OUTPUT

first

second

It’s unwanted to have leftover channels and goroutines to consume resources and cause leaky applications. So it’s necessary to close channels and exit goroutines.

The predominant method for avoiding unsafe channel closing is to use additional channels to notify goroutines when it’s safe to close a channel.

WAITGROUP

A waitgroup is used to wait for the goroutines to complete, it’s a counting semaphore. Use wg.Add(n) to increment the count and wg.Done() to decrement it.

 A call to wg.Wait() will block the calling goroutine until the wait group's value is zero.

Also the select statement is used in the following code: it checks each case condition to see whether any of them have a send or receive operation that needs to be performed. 

If exactly one of the case statements can be sent or received, select will execute that case. 

If more than one can send or receive, select randomly picks one. If none of the case statements can send or  receive, select falls through to a default (if specified). 

And if no default is specified, select blocks until one of the case statements can send or receive.

OUTPUT

Done channel

done something

done other

The defer statement pushes a function call onto a list, and the list of saved calls is executed after the surrounding function returns.

It is commonly used to simplify functions that perform various clean-up operations, such as closing a file handle, or to decrement a WaitGroup.

 Deferred statements run even if the function results in a panic.

MUTEX

We can use a mutex to safely access data across multiple goroutines. This mutex will synchronize access to state. 

It allows a mutual exclusion on a shared resource (no simultaneous access).

OUTPUT

1000

Lock() the mutex to ensure exclusive access to the state, Unlock() the mutex, and increment the count. 

sync.RWMutex is a reader/writer mutex. It provides the same methods that we have just seen with Lock() and Unlock() (as both structures implement sync.Locker interface). Yet, it also allows concurrent reads using RLock() and RUnlock() methods: 

A sync.RWMutex allows either at least one reader or exactly one writer whereas a sync.Mutex allows exactly one reader or writer. 

Locking/unlocking a sync.RWMutex is faster than locking/unlocking a sync.Mutex. On the other end, calling Lock()/Unlock() on a sync.RWMutex is the slowest operation.

sync.Once

sync.Once is a simple and powerful primitive to guarantee that a function is executed only once. 

It’s useful to set up configurations only once.

OUTPUT

Run once first time only

Run this

Run this

These are some basic concepts, the sync package offers more functionality like basic synchronization primitives. 

Other than the Once and WaitGroup types, most are intended for use by low-level library routines. 

Higher-level synchronization is better done via channels and communication.  You can investigate more in https://pkg.go.dev/sync

By Luciano Ferrari

Software Developer