Ir al contenido principal

Concurrency in Go: Goroutines

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:

  1. This one in which the Go concurrency foundation, the goroutines, are analyzed.
  2. Synchronization between subprocesses: Go waitgroups
  3. 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.

Entradas populares de este blog

Linting C# in Visual Studio Code

Though very usual in programming environments as Javascript/Typescript, linting , or analyzing code for enforcing a set of coding style rules, is not usually present in the .NET based environments. Rule enforcing is really useful when working on team shared codebases in order to keep them coherent, what in last term reduces both development times and coding errors. A linting example Maybe a practical example would be helpful for explaining what  linting  is to the newcomers (feel free to go on if you aren't). Let's imagine you are a new member in a C# development team that has well established set of coding style rules. Instead (or apart) of putting them in a document, they've adopted a tool that checks these rules during the code building process. Your first code is such ambitious as this: namespace HelloWorld {      using System;      public class Program      {           public static void Main(string[] args)           {                Console

ESlint: Ignore unused underscore variables

Some naming conventions promote the use of the underscore character (" _ ") for those variables that must be declared but are not being used. One common case is that in which a function signature contains some variables that will not be used, as for instance the Express error handlers: app.use(function(err, req, res, next) { console.error(err.stack); res.status(500).send('Something broke!'); }); In the above example only the arguments err and res are being used, though all four must be defined in the handler signature. Thus, following the naming convention of using underscores for those unused variables, we could recode it as: app.use(function(err, _, res, __) { console.error(err.stack); res.status(500).send('Something broke!'); }); Though it makes the function more readable, it comes with a problem if using ESlint: it will blame by declaring unused variables. error '_' is defined but never used error '__' is define

Using Bitbucket app passwords with git on MacOS (OSX)

Learn how Bitbucket passwords are stored by git on MacOS.