Introduction
Rob Pike, One of the creators of Go in his 2012 Google I/O talk referred to concurrency as the composition of independently executing computations. Concurrency not to be confused with parallelism is a scenario whereby different computational tasks are executed in overlapping periods, in an out of order fashion that doesn't affect the overall result of the program! Goroutines and channels are core components of concurrency in Go. This article aims to show how to use Goroutines and channels to write concurrent Go programs.
Prerequisites
To follow along with this article you'll need:
- Go installed on your computer.Depending on your os you can follow the following articles to get started with it Mac, Windows,Ubuntu
- Knowledge of how to convert data types in Go, You can follow this article to know more
- To know how to construct for loops in Go.
Goroutines
Goroutines can be thought of as lightweight,user-space threads that are managed by the Go runtime, Once a new goroutine is created, it gets its own call stack, the go runtime allocates a few flexible kilobytes to it which can increase and decrease in size according to its needs, this prevents excess memory usage and accounts for the lightweight nature of goroutines. They are functions executing concurrently with other goroutines in the same address space. The purpose of a goroutine is to have several independent functions executing at the same time incorporated onto a thread or set of threads such that once the execution of one function stops or is blocked due to a blocking operation like a network request the execution of the other functions wouldn't stop! Instead, they would be moved to a different thread on the operating system to ensure that there is no pause in the execution of these functions! This task of scheduling various goroutines, moving them to available OS threads when there is a blocking operation is carried out by the Go-runtime scheduler. Goroutines are very cheap and require little system resources that is why it is possible to have several hundreds of goroutines in your programs, In the next section we will be going ahead to see how we are going to write some code and see how we can get started using goroutines in our programs.
Goroutines in action
By default, whenever the main function is created in a go file, the runtime automatically sets up one goroutine. This can be confirmed using the numGoRuoutine()
method on the runtime package! when this function executes, it outputs the number of goroutines being run, in this case, "1" is outputted implying that only one goroutine is created, This goroutine is the one created by default, thanks to the goruntime!
package main
import (
"fmt"
"runtime"
)
func main () {
fmt.Print("No Of Goroutine: ", runtime.NumGoroutine())
}
The runtime package is imported into the main.go file as seen above, the result of invoking the runtime.numGoRuoutine()
method is printed out.
Here's output of the above file when it is executed:
No Of Goroutine: 1
Additional goroutines can be added to the default one by adding the "go" keyword before the name of the function! whenever the "go" keyword precedes a function call, a new goroutine is created! Several goroutines can be created but it is important to note that there might be some bad consequences if used wrongly!
package main
import (
"fmt"
"time"
)
func main() {
greeting("Hello")
greeting("Toby")
}
func greeting(s string) {
for i := 0; i true; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(i,s)
}
}
Here's the output of the above file when it is executed:
0 Hello
1 Hello
2 Hello
3 Hello
4 Hello
5 Hello
//Continues printing out hello till your system crashes ):
from the snippet above two functions can be seen! The main function and another custom function called greeting! The greeting function receives an argument of type string and the for loop in it prints out the index and the name of the argument received! This greeting function is called twice in the main function with different arguments of type string. However, when the file is executed, only the first function call with the argument "Hello" is printed to the terminal infinitely, the second function call with the argument "Toby" never runs, this is because the first function call is blocking, it runs infinitely and the second one never has a chance to run. This problem can be dealt with by adding the go keyword in front of the first function call, the goruntime creates a new goroutine and runs the two functions concurrently it says you know what, I want the both of you to run independently of each other and when the main.go file is executed, the output shows "Hello" the argument of the first function call and "Toby" the argument of the second function call being printed out to the terminal concurrently!
package main
import (
"fmt"
"time"
)
func main() {
go greeting("Hello")
greeting("Toby")
}
func greeting(s string) {
for i := 0; i <= 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(i,s)
}
}
Here's the output of the above file when it is executed:
0 Toby
1 Toby
1 Hello
2 Hello
2 Toby
3 Hello
3 Toby
4 Hello
4 Toby
5 Hello
5 Toby
The runtime.NumGoroutine()
function used earlier on can be used to confirm if a new goroutine was truly created! Since the runtime.NumGoroutine() function returns a value of type int, It must be converted to a type string before being passed to the greeting function!when the file is executed it outputs the number 2 different from the previous one this indicates that a new goroutine has truly been created
package main
import (
"fmt"
"strong"
"runtime"
"time"
)
func main() {
go greeting("Hello")
n := strconv.Itoa(runtime.NumGoroutine())
greeting("Number of GoRoutines: " + n)
}
func greeting(s string) {
for i := 0; i <= 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(i,s)
}
}
Here's the output of the above file when it is executed:
0 Number of GoRoutines: 2
0 Hello
1 Hello
1 Number of GoRoutines: 2
2 Hello
2 Number of GoRoutines: 2
3 Hello
3 Number of GoRoutines: 2
4 Hello
4 Number of GoRoutines: 2
5 Number of GoRoutines: 2
We see that the number of goroutines has increased. Several goroutines can be created in a function depending on the needs of the function, thanks to the lightweight nature of goroutines. An Attempt to duplicate the call to greeting and create another goroutine inside the main function would result in weird behavior, This is because when a goroutine is started it returns immediately and the next line of code is executed! In this case when a new goroutine is started it immediately returns and tries to execute the next line of code, the goruntime notices and says hey, you are also a function call that creates a goroutine I am going to return you immediately after returning the second goroutine it searches for another line to be executed! It can't find it so the runtime says I'm done and terminates the main function without waiting for the other goroutines to be done and that is why nothing is outputted! The goroutine is unable to continue running without the help of the main function! Adding a single line of code fmt.scanln()
will ensure that the main function is not terminated till all the other goroutines are done or there is a form of user-input to the terminal. This hack is quite tacky and wouldn't scale in a real-life scenario and hence we need WaitGroups which are more efficient in the synchronization of goroutines!
WaitGroups
The waitgroup is a type under the Go sync package! It ensures synchronization between various goroutines. It waits for the collection of the goroutines to be finished before terminating the main function, this is to prevent weird behaviors as seen above! The waitgroup ensures the synchronization between various goroutines using a counter system! The main function calls the Add method with the number of goroutines to wait for passed in as an argument and each goroutine calls Done to indicate that they have finished execution, The third method provided by the waitgroup is the Wait() method which blocks the main function! once any goroutine calls the done method the waitgroup argument is decremented by one. whenever the argument becomes 0, the wait method is returned and the main function can run!
package main
import (
"fmt"
"time"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(1)
go greeting("Hello", &wg)
go greeting("Toby", &wg)
wg.Wait()
}
func greeting(s string, wg *sync.WaitGroup) {
for i := 0; i <= 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(i,s)
}
wg.Done()
}
Here's the output of the above file when it is executed:
0 Toby
0 Hello
1 Hello
1 Toby
2 Hello
2 Toby
3 Toby
3 Hello
4 Toby
4 Hello
5 Hello
5 Toby
The snippet above solves the blocking issue that was encountered above! A variable wg of type sync.WaitGroup is created in the main function and the Add method is called with the waitGroup counter The greeting function is refactored and made to receive an argument of type pointer to sync.waitgroup! In the main function, the two greeting functions are called with a pointer to the type sync.WaitGroup and a string and lastly the wait() method is invoked. When the values are outputted, we can see that it does not block even though we created two goroutines. This is because the waitGroups have done the job of synchronizing the goroutines in such a way that they run concurrently! It is important to note that WaitGroups must not be copied after first use as this can result in a panic error if not handled properly! WaitGroups are useful and essential, however, A need might arise whereby two goroutines would need to communicate and pass messages between each other. The waitGroups are unable to pass messages between various goroutines so we have to get the help of Channels to enable the goroutines to communicate, we would discuss channels in the next section!
Channels
Channels are used for communication and sometimes synchronization between two goroutines! Channels are by default bidirectional, this means that anyone with access can read or write data. There are two main operations channels perform, Sending, and receiving. These operations are blocking, This is to ensure that there is effective synchronization between the various goroutines. Only one goroutine can have access to the data at a given time this design consideration is to prevent a data race condition which occurs when two goroutines access the same variable concurrently and at least one of the accesses is a write, the slogan "Do not communicate by sharing memory; instead, share memory by communicating." which is popular within the Go community aims to encourage this thinking. Channels usually have a data type associated with it, This data type is the only type of data that is allowed to be communicated to the goroutines! Channels are defined using the make function, just as it is when defining any other variable of type slice or map! VariableName:= make(chan value-type)
is the format used to create an unbuffered channel with the name variableName and type value-type(The type you intend to pass across goroutines). We can indicate the direction of the data flow by using an arrow ch <- data this sends data to the channel and v:= <- ch
receives the data from the channel!
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan string)
go greeting("Hello", ch)
for {
msg := <- ch
fmt.Println(msg)
}
}
func greeting(s string, ch chan string) {
for i := 0; i <= 5; i++ {
//WE SEND A VALUE THROUGH A CHANNEL
ch <- s
time.Sleep(100 * time.Millisecond)
}
}
Channels can be passed as arguments for functions as seen in the example above! The channel is created using the make function and the chan keyword, it is then passed on to the call to the greeting function which accepts a string and a channel of type string! A for loop is then created in the main channel to receive and print out every incoming message! The output when the file is executed is as seen below!
Hello
Hello
Hello
Hello
Hello
Hello
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
/test/ttest/code/go/src/main.go:204 +0x96
exit status 2
Although the output is received and correctly printed out, a fatal error that reads "all goroutines are asleep - deadlock!" is seen. This error occurred because the main function keeps waiting to receive new messages even though the greeting function has completed sending any new messages. This can be fixed by adding a close() function with the channel as an argument to the sender function and refactoring the for loop to range over the channels so it only expects the exact number of messages being sent as seen below!
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan string)
go greeting("Hello", ch)
for msg := range ch {
msg = <- ch
fmt.Println(msg)
}
}
func greeting(s string, ch chan string) {
for i := 0; i <= 5; i++ {
ch <- s
time.Sleep(100 * time.Millisecond)
}
close(ch)
}
when the above code is executed, the correct output will be gotten and we won't see the funky deadlock error again which is great!
There are two types of channels you can have in Go, Buffered & Unbuffered channels. All the instances of channels seen in the examples above are unbuffered channels! they are unbuffered because they do not have any buffer or hold any value! In simpler terms, the channels seen above can only send and receive one value, unbuffered channels ensure that the sending and receive operations are performed immediately they are initiated as seen above. The second type of channel, buffered channels contain a buffer that is used to provision values before they are received. The buffer size is usually greater than 0 and ensures that the goroutine is not blocked until the buffer size is full. The buffered channels do not require the goroutines to perform send and receive operations at the same time. Buffered channels are created by the non-zero size or capacity of the buffered channel as an extra argument as seen below. *ch := make(chan type, capacity)*
. The buffer size indicates the number of values that the channel can hold, the behavior of the send and receives blocking operations are slightly different for a buffer channel, in this case, the goroutine will block if there are no values in the channel to receive or if there is no available buffer to place the value being sent, i.e the buffer is full!
package main
import (
"fmt"
)
func main() {
chan := make(chan string, 3)
ch <- "Hello"
ch <- "World"
ch <- "I'm a Gopher"
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
}
The example seen above is a simple program that makes use of buffered channels to send and receive data and print the values out! The size of the buffer channel is 3 and if you try to send more than the size of the channel, a deadlock error would occur!
Conclusion
So far this article has been able to give a broad overview of Concurrency in Go, Channels, Waitgroups, and Goroutines! These are essential concepts in Golang and I hope this article was able to help shed more light on these concepts!
If you enjoyed this article and feel up to it, please do share it! If you have any questions, leave a comment! I'll be there to answer it! You can connect with me on twitter @ghvstcode
Top comments (4)
Thanks for this.
I found this article to be helpful as I am learning concurrency, however I have one slight correction as I followed your code.
In the second to last code snippet you shared, line 13 is "msg := <- ch" and should instead be "msg = <- ch", reassigning the value of msg or the code breaks.
Hey Rob, I'm glad you found this article helpful! Thanks for pointing me to the error, it was a silly typo! will fix it right away
Succinct