After a long break, I have decided to put together a set of Advanced Go tutorials to help fellow gophers to learn more about this powerful programming language. This post assumes that you have a working knowledge with Go and you are here to learn even further. Today, we are going to delve into one of the most important topics in any programming language, Concurrency.
Go is both a low-level systems and a great general purpose language and one of its primary strengths is the built-in concurrency model and tools. Many programming languages have third-party libraries for concurrency, but inherent concurrency is what makes Go stand out, since it is a core feature of its design.
Goroutines
In Go, the concurrency is handled via a method/function called
package main
import (
"log"
"time"
)
func print(s string) {
for i := 0; i < 5; i++ {
log.Println(s, i)
time.Sleep(100 * time.Millisecond)
}
}
func main() {
// Let's create a boolean channel to prevent main func from terminating.
waitc := make(chan bool)
// This new goroutine will execute concurrently with the calling one.
go print("goroutine")
// You can also start a goroutine for an anonymous function call.
go func(msg string) {
log.Println(msg)
}("Running")
// Timer to end the program after 3 seconds.
go func() {
time.Sleep(time.Second * 3)
log.Println("Exiting")
waitc <- true
}()
// Regular function call.
print("function call")
<-waitc
}
In this example, we create a function called "print" and we call this function in a Goroutine and as a regular function. Being asynchronous, when a goroutine is invoked, it waits for the blocking code to complete before concurrency begins. This is the reason of seeing "function call 0" printed out first, even though it's placed below the goroutines in the main function.Unlike regular function calls, the control does not wait for a Goroutine to finish its execution. It returns immediately to the next line of code after the Goroutine call and any return values from the Goroutine are ignored. The main Goroutine has to be running for any other Goroutines to be able to continue running. If the main Goroutine terminates then the program terminates along with all the running Goroutines. In order to prevent the main function from terminating, we created a channel called "waitc" in the example above that we read from at the end of the main function (Basic sends and receives on channels are blocking). This allows us to terminate the program whenever we want and we did terminate it using a timer in a separate Goroutine. Here is a sample output(yours will be likely different):
2018/02/25 21:48:02 function call 0
2018/02/25 21:48:02 goroutine 0
2018/02/25 21:48:02 Running
2018/02/25 21:48:02 function call 1
2018/02/25 21:48:02 goroutine 1
2018/02/25 21:48:02 function call 2
2018/02/25 21:48:02 goroutine 2
2018/02/25 21:48:02 goroutine 3
2018/02/25 21:48:02 function call 3
2018/02/25 21:48:03 goroutine 4
2018/02/25 21:48:03 function call 4
2018/02/25 21:48:05 Exiting
Channels
Channels are a typed conduit through which you can send and receive values with the channel operator, <-. Read and write operations on channels are blocking by default and need to be handled with care. It's also possible to create buffered channels to be able to do multiple read-writes concurrently. Let's take a look at this example to understand how channels work:
package main
import (
"log"
)
func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c)
}
func main() {
// We create a buffered channel with capacity 10
c := make(chan int, 10)
go fibonacci(cap(c), c)
// Range receives values from the channel repeatedly until it is closed.
for i := range c {
log.Println(i)
}
}
Channels aren't like files; you don't usually need to close them. Closing is necessary when the receiver must be told there are no more values coming, such as to terminate a range loop like in our example above. Here is the output of the program:
2018/02/25 22:14:34 0
2018/02/25 22:14:34 1
2018/02/25 22:14:34 1
2018/02/25 22:14:34 2
2018/02/25 22:14:34 3
2018/02/25 22:14:34 5
2018/02/25 22:14:34 8
2018/02/25 22:14:34 13
2018/02/25 22:14:34 21
2018/02/25 22:14:34 34
Before we move on to the next section, I'd like to share a beautifully written post named Visualizing Concurrency. The author brilliantly puts together different examples of concurrency and provides a visual representation of the program flow. I strongly recommend you to take a look at it.
Locks
We've seen how goroutines use channels to communicate with each other, but what if we need different goroutines to modify a data structure or write to a stream concurrently. Do you see the problem here? If more than one goroutine attempts to update a data structure, they will overwrite each others' changes. How can we prevent this behaviour?We can prevent this behaviour using
package main
import (
"log"
"sync"
"time"
)
// SafeMap is safe to use concurrently.
type SafeMap struct {
data map[string]string
mu sync.Mutex
}
// Write writes to the SafeMap.
func (s *SafeMap) Write(key string, value string) {
// We lock so only one goroutine at a time can access the map
s.mu.Lock()
defer s.mu.Unlock()
s.data[key] = value
}
// Read prints the current value of the map for the given key.
func (s *SafeMap) Read(key string) {
s.mu.Lock()
defer s.mu.Unlock()
log.Println(s.data[key])
}
func main() {
waitc := make(chan bool)
s := SafeMap{data: make(map[string]string)}
go s.Write("index1", "value1")
go s.Write("index2", "value2")
go s.Write("index3", "value3")
go s.Write("index4", "value4")
go s.Write("index5", "value5")
time.Sleep(time.Second * 1)
for k := range s.data {
go s.Read(k)
}
go func() {
time.Sleep(time.Second * 3)
waitc <- true
}()
<-waitc
}
In this example, we created a SafeMap implementation that allows multiple goroutines to write and read on it using Mutexes. If you would like to see the race condition when there is no locking mechanism used in this example, simply comment out Lock() and Unlock() lines in the "Write" method.
If we would like to improve this example a bit, we can use a
type SafeMap struct {
data map[string]string
mu sync.RWMutex // sync.RWMutex provides more granular control over sync.Mutex
}
// Notice how the methods start with R when using RWMutex for read operations.
func (s *SafeMap) Read(key string) {
s.mu.RLock()
defer s.mu.RUnlock()
log.Println(s.data[key])
}
Defer
On a final note, I'd like to talk about the
func main() {
db := NewDB('database')
defer db.Close()
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
I hope this post helps you to understand the fundamentals of Go's concurrency features. Please play with the examples and try to understand the behaviour of the code. Feel free to drop a line below if you have any questions or comments. Cheers and happy coding!
Disqus Comments Loading..