This post will demonstrate how to do inter-thread communication in Golang concurrent programming. Generally speaking, there are two ways for this fundamental question:
shared memory and
message passing. You’ll see how to do these in Golang based on a case study and some tricky problems around it.
Golang is a new popular and powerful programming language that aims to provide a simple, efficient, and safe way to build multi-threaded software.
Concurrent programming is one of Go’s main selling points.
Go uses a concept called goroutine as its concurrency unit. Goroutine is a complex but interesting topic, you can find many articles about it online, This post will not cover concepts about it in detail. Simply speaking, goroutine is a user-space level thread which is lightweight and easy to create.
As mentioned above, one of the complicated problems when we do concurrent programming is that inter-thread (or inter-goroutine) communication is very error-prone. In Golang, it provides frameworks for both
shared memory and
message passing. However, it encourages the use of channels over shared memory.
You’ll see how both of these methods in Golang based on the following case.
The example is very simple: sums a collection (10 million) of integers. In fact this example is based on this good article. It used the
shared memory way to realize the communication between goroutines. I expand this example and implemented the
message passing way to show the difference.
Go supports traditional shared memory accesses among goroutines. You can use various traditional synchronization primitives such as lock/unlock (Mutex), condition variable (Cond) and atomic read/write operations(atomic).
In the following implementation, you can see Go uses
WaitGroup to allow multiple goroutines to do their tasks before a waiting goroutine. This usage is very similar to
pthread_join in C.
Goroutines are added to a WaitGroup by calling
Add method. And the goroutines in a WaitGroup call
Done method to notify their completion, while a goroutine make a call to
Wait method to wait for all goroutines’ completion.
In the example above the
v is shared across goroutines. When this variable needs to be updated, an atomic operation was done by calling
atomic.AddInt64() method to avoid race condition and nondeterministic result.
That’s how shared memory across goroutines works in Golang. Let’s go to message passing way in next section.
In Golang world, there is one sentence is famous:
Don’t communicate by sharing memory; share memory by communicating
Channel(chan) is introduced in Go as a new concurrency primitive to send data across goroutines. This is also the way Golang recommended you to follow.
So the concurrent program to sum 10 million integers based on
Channel goes as below:
To create a typed channel, you can call
make method. In this case, since the value we need to pass is an integer, so we create an int type channel with
c := make(chan int). To read and write data to that channel, you can use
<- operator. For example, in the
add goroutine, when we get the sum of integers, we use
c <- v to send data to the channel.
To read data from the channel in the main goroutine, we use a build-in method
range in Golang which can iterate through data structure like slice, map and channel.
That’s it. Simple and beautiful.
Let’s build and run the above solution. You’ll get an error message as following:
fatal error: all goroutines are asleep - deadlock!
deadlock issue occurs because of these two reasons. Firstly by default sends and receives to a channel are blocking. When a data is send to a channel, the control in that goroutine is blocked at the send statement until some other Goroutine reads from the channel. Similarly when data is read from a channel, the read is blocked until some Goroutine writes data to that channel. Secondly,
range only stops when the channel is closed. In this case, each
add Goroutine send only one value to the channel but didn’t close the channel. And the main Goroutine keeps waiting for something to be written (in fact, it can read 4 values, but after that it doesn’t stop and keep waiting for more data). So all of the Goroutines are blocked and none of them can continue the execution. Then hit a deadlock.
Let use the manual
for loop, in each iteration we read the value from the channel and sum together. Run it again. The deadlock is resolved.