This is the first article of a series analyzing Go concurrency, ie, parallel execution of different threads using Go. The series is composed of these articles:
- This one in which the Go concurrency foundation, the goroutines, are analyzed.
- Synchronization between subprocesses: Go waitgroups
- Data exchange between subprocesses: Go channels
Goroutines
In brief, a goroutine is a standard Go function that is processed in parallel (concurrently) to the main Go process.
Let's see an example. The execution of the next code is quite predictible: The main Go process (that code in the main()
function) will execute, then it will pause while the function mySubprocess()
is running and then it will finish. Just one process, no concurrent execution.
package main
import (
"fmt"
"time"
)
// mySubprocess sleeps for a second
func mySubprocess() {
fmt.Println("Entering to mySubprocess")
time.Sleep(1 * time.Second)
fmt.Println("Exiting from mySubprocess")
}
// main program process
func main() {
fmt.Println("Calling mySubprocess from main()...")
mySubprocess()
fmt.Println("mySubprocess finished!")
}
(Try the above code in Go Playground)
As expected, the above code outputs:
Calling mySubprocess from main()...
Entering to mySubprocess
Exiting from mySubprocess
mySubprocess finished!
Now, if we want to process mySubprocess()
concurrently to the main process, ie, if we want to run it in parallel, we must to convert it into a goroutine by calling it prepended by the reserved word go
package main
import (
"fmt"
"time"
)
// mySubprocess sleeps for a second
func mySubprocess() {
fmt.Println("Entering to mySubprocess")
time.Sleep(1 * time.Second)
fmt.Println("Exiting from mySubprocess")
}
// main program process
func main() {
fmt.Println("Calling mySubprocess from main()...")
go mySubprocess()
fmt.Println("mySubprocess finished!")
}
(Try the above code in Go Playground)
Surprisingly, the output of the above code is not what we could expect:
Calling mySubprocess from main()...
mySubprocess finished!
Why mySubprocess()
didn't run? It didn't run because the main process finished before running it.
Let's try again, this time setting a rudimentary wait mechanism in the main process:
package main
import (
"fmt"
"time"
)
// mySubprocess sleeps for a second
func mySubprocess() {
fmt.Println("Entering to mySubprocess")
time.Sleep(1 * time.Second)
fmt.Println("Exiting from mySubprocess")
}
// main program process
func main() {
fmt.Println("Calling mySubprocess from main()...")
go mySubprocess()
fmt.Println("mySubprocess called!")
time.Sleep(2 * time.Second)
fmt.Println("main() finished")
}
(Try the above code in Go Playground)
In the above code, a call to time.Sleep()
has been added after calling mySubprocess()
, sleeping two times the ellapse that mySubprocess()
sleeps to ensure it waits until it finishes. The output of the above code is:
Calling mySubprocess from main()...
mySubprocess called!
Entering to mySubprocess
Exiting from mySubprocess
main() finished
As can be seen, the subprocess has run while the main process was running... though, in rigor, the main process was not running but sleeping, what is not a real concurrent execution.
A real example
To evaluate Go multithreading in real conditions, the next example implements the typical hard processing operation: The calculation of the Fibonacci sequence for a given number.
The main program launches 8 goroutines, each calculating the Fibonacci sequence for 27, 26... and 20, and then the main program calculates the Fibonacci sequence of 210 as a wait mechanism to allow goroutines to complete.
package main
import (
"fmt"
"math"
)
// getFibonacci calculates the Fibonacci sequence
// for a given number
func getFibonacci(n float64) float64 {
if n <= 1 {
return n
}
n2, n1: = 0.0, 1.0
for i := 2.0; i <= n; i++ {
n2, n1 = n1, n1 + n2
}
return n1
}
// mySubprocess calculates the Fibbonacci sequence
// for a given number and prints it
func printFibonacci(n float64) {
fmt.Printf(
"Calculating the Fibbonacci sequence for number %.0f\n", n)
v: = getFibonacci(n)
fmt.Printf(
"The Fibonacci sequence for %.0f is %.0f\n", n, v)
}
// main process
func main() {
for i := 7; i >= 0; i-- {
go printFibonacci(math.Pow(2, float64(i)))
}
printFibonacci(math.Pow(2, float64(10)))
}
To ensure that the above example runs in a multithreading environment, avoid running it in the Go Playground or even in debugging mode. Instead, generate a binary and run it.
The output will vary from one execution to another, but in all the cases it will be noted that the subprocesses calculating the Fibonacci sequences are run concurrently:
Calculating the Fibbonacci sequence for number 1024
Calculating the Fibbonacci sequence for number 16
The Fibonacci sequence for 16 is 987
Calculating the Fibbonacci sequence for number 8
The Fibonacci sequence for 8 is 21
Calculating the Fibbonacci sequence for number 2
The Fibonacci sequence for 2 is 1
Calculating the Fibbonacci sequence for number 32
Calculating the Fibbonacci sequence for number 1
The Fibonacci sequence for 1 is 1
Calculating the Fibbonacci sequence for number 128
The Fibonacci sequence for 128 is 251728825683549523871268864
The Fibonacci sequence for 32 is 2178309
The Fibonacci sequence for 1024 is 4506699633677816191404865591201603611210057765586363088692424961083421629061324540306009631764407814868917761514659447075449365476418924571096193010086458680628417980162101749952294888691146652624641609216913571840
Note that if you omit the reserved word go
and run the above program without using goroutines, the output would be:
Calculating the Fibbonacci sequence for number 128
The Fibonacci sequence for 128 is 251728825683549523871268864
Calculating the Fibbonacci sequence for number 64
The Fibonacci sequence for 64 is 10610209857723
Calculating the Fibbonacci sequence for number 32
The Fibonacci sequence for 32 is 2178309
Calculating the Fibbonacci sequence for number 16
The Fibonacci sequence for 16 is 987
Calculating the Fibbonacci sequence for number 8
The Fibonacci sequence for 8 is 21
Calculating the Fibbonacci sequence for number 4
The Fibonacci sequence for 4 is 3
Calculating the Fibbonacci sequence for number 2
The Fibonacci sequence for 2 is 1
Calculating the Fibbonacci sequence for number 1
The Fibonacci sequence for 1 is 1
Calculating the Fibbonacci sequence for number 1024
The Fibonacci sequence for 1024 is 4506699633677816191404865591201603611210057765586363088692424961083421629061324540306009631764407814868917761514659447075449365476418924571096193010086458680628417980162101749952294888691146652624641609216913571840
Goroutine limitations
One of the limitations of the goroutines has been shown before: The main program doesn't wait until the running goroutines finish their execution. This limitation can be easily bypassed by using waitgroups, a concept that is covered by the second article of this series.
The second limitation is return values: As shown in the previous examples, any of the functions run as goroutines return values, and that's not casual. A function run as goroutine cannot return values as a normal function.
There are several ways of bypassing this limitation but the more natural is using channels, a concept that is exposed in the third and last article of this series.