Introduction
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.
Background
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.
Case study
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.
Shared Memory
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.
1 | package main |
In the example above the int64
variable 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.
Message Passing
In Golang world, there is one sentence is famous:
Don’t communicate by sharing memory; share memory by communicating
For that, 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:
1 | package main |
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.
Hit the Deadlock
Let’s build and run the above solution. You’ll get an error message as following:
1 | fatal error: all goroutines are asleep - deadlock! |
The 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.
Fix the Deadlock
1 | package main |
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.