Go routines are the building blocks of concurrency in Golang. Golang, which is known for it’s simplicity has made handling concurrency as easy as writing go in front of any function. In this series we will deep dive into golang concurrency patterns, and one by one unearth the basics and thier use cases in concurrent programming. In this particular article, name Golang Concurrency : Goroutines, we will deep dive into goroutines, you think you know goroutines, well read this article and think again.

All Articles of the Series :
1. Golang Concurrency : Goroutines

2. Golang Concurrency : Channels

3. Golang Concurrency: Select and For Range Channel

A simple Goroutine

package main

import "fmt"

func goRoutine() {
	fmt.Println("I am a goroutine")
}
func main() {
	go goRoutine()
	fmt.Println("I am main")
}

// Output 
I am main

And this is it, we decalre a function, just a normal function (goRoutine), and in main call it via go keyword to make it run concurrently as a goroutine. Easy-Peasy, right??

Wait, Why I am not seeing I am a goroutine in tht output?

And here we start, The caller function, here main does not wait for the goroutines to finish. As soon as main finishes the execution and reaches the end, it terminates the program, hence the goroutine never gets executed.

Acutally not never, this is probabilistic, if the compiler decides to run the goroutine first, you can see the statement in the output, else not. Go try it, run the program 1000 times, you might see the I am a goroutine once or twice.

But how to make sure that the goroutine gets completly executed?

Try giving the main function a breather?? Give it some sleep time, which will allow the compiler to run the goroutine. Let us try it.

package main

import "fmt"
import "time

func goRoutine() {
	fmt.Println("I am a goroutine")
}
func main() {
	go goRoutine()
	time.Sleep(time.Second)
	fmt.Println("I am main")
}

// Output
I am a goroutine
I am main

This is not at all a good way to make sure that your goroutines execute. The more elegant way is using the sync packages, but that is not the aim of this article, this articles is aimed to make you visualize what on Earth a goroutine is, and that is where we move on to next.

Goroutines are not OS Threads

For most of the first timers, specially from JAVA or Python background, goroutines sound similar to thread, and this my friends is a complete fallacy. Infact in Golang, you never need to think about threads, the Golang runtime takes care of that. Why golang opted for goroutines over OS Threads?

  • Size:
    Goroutines are much much lighter than threads.
  • Management:
    Threads are managed by host OS, goroutines can be managed by the go runtime.
  • Communication:
    OS Threads need complicated techniques like Mutex, Semaphores, etc to communicate, goroutines communicate via channels.
  • Context Switching:
    Switching between goroutines is very easy, that is why the runtime can handle thousand of goroutines effectively.

Actually, goroutines are built on top of OS threads. A certain number of goroutines are mapped to a thread.

Goroutines and CPU

Since goroutines are mapped to OS Threads, it is given that there is a relationship between goroutines and CPU as well. A CPU is often divided into CPU Cores. Each CPU core is capable of executing it’s own set of instructions independently of the other cores. CPU cores helps in excuting multiple tasks simultaneously.

Goroutines make use of cores to execute tasks concurrently. Let us now try to figure out the number of CPU cores my system has and the number of cores that golang runtime can use simultaneously.

package main

import (
	"fmt"
	"runtime"
)

func main() {

	fmt.Println(runtime.NumCPU())
	fmt.Println(runtime.GOMAXPROCS(0))
}

// Output
// 8
// 8

Golang provides runtime packages to get more information about the runtime. We have used NumCPU() to ge the number of CPU Cores and GOMAXPROCS() to get the number of CPU cores the golang runtime is allowed to use.

My Macbook has 8 CPU cores, and by default the go runtime uses all the available CPUs for execution.

Actually the GOMAXPROCS functions sets the number of cores to be used, but when the you pass the value less than 1, it returns the number of cores currently being used, hence the 0 in the params.

Each CPU Core can run 1 goroutine at a given time. Thus we can use this to control the concurrency of the go runtime.

So if we set the GOMAXPROCS to 1, each goroutine will actually run sequentially (one after the other) and not concurrently. Let us see some examples.

package main

import (
	"fmt"
	"runtime"
	"sync"
)

var wg sync.WaitGroup

func iterate(n int) {
	defer wg.Done()

	for i := 0; i <= n; i++ {
		fmt.Print(i)
	}
	fmt.Println("")
}
func main() {

	runtime.GOMAXPROCS(1)
	wg.Add(3)
	go iterate(10)
	go iterate(10)
	go iterate(10)
	wg.Wait()
}
// Output
// 012345678910
// 012345678910
// 012345678910

As you see, we have set the GOMAXPROCS to 1, and each goroutine runs sequentially.
Now let us remove the limit, and see how the code behaves. (I will also add a sleep of 1 microsecond in every iteration just to bring out the race condition evidentally).

package main

import (
   "fmt"
   "sync"
   "time"
)

var wg sync.WaitGroup

func iterate(n int) {
   defer wg.Done()

   for i := 0; i <= n; i++ {
      time.Sleep(time.Microsecond)
      fmt.Print(i)
   }
   fmt.Println("")
}
func main() {
   wg.Add(3)
   go iterate(10)
   go iterate(10)
   go iterate(10)

   wg.Wait()

}

// Ouptput
//00011223344551626377488596109
//107
//8910

Infact this output is not deterministic, every re run will give you different outputs, because the code is running concurrently.

Race Conditions

Race conditions go hand in hand with concurrent programming , and hence with goroutines.
Race conditions occurs when several goroutines try to access the same shared data without proper synchronisation. Because the execution of goroutines is non deterministic , if there is no synchronisation mechanism, the state of the shared data at any given time will be non deterministic, which might lead to bugs or even crashes. Let us again see an example.

import (
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup
var counter = 0

func increment() {
	defer wg.Done()
	counter = counter + 1
}

func main() {
	for i := 0; i < 3500; i++ {
		wg.Add(1)
		go increment()
	}
	wg.Wait()
	fmt.Println(counter)

}
// Output
// 3063

As you see here, the output of this counter is not 3500, it changes with every execution, the reason is race condition. You don’t know the order in which the goroutines are running and thus the order in which the counter variable is getting incremented, hence the final number is not 3500.

Wanna see a simpler case :

import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup

func main() {

	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			fmt.Print(i)
			fmt.Print("  ")
		}()
	}
	wg.Wait()

}
// Output
// 10  3  610  10    10  10  10  10  10  done

In this case, we are running inside a loop and all are referencing to a looping variable (i in this case). Please look closely, we are not passing the value of i to each goroutine, we are referencing the value of i (because it is in the same scope) .In such scenarios there might be a case that the value of i is same for 2 or more goroutines.
Now let us look at a case where race conditions can lead to crashes.

import (
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup

func main() {

	var myMap = map[int]int{1: 1, 2: 2, 3: 3}

	for i := 0; i < 100; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()

			myMap[i] = i
		}()
	}
	wg.Wait()
	fmt.Println("done")

}

This code will crash, with the reason being fatal error: concurrent map writes. Let us deep dive, what happens here. As stated above, when you run goroutines inside the for loop, and reference the looping variable (i in this case), you can never be sure which value of i will be captured when the goroutine is executed. It might happend the two or more goroutines capture the same value of i thus trying to write on the the same key and thus crashing.
A safer solution might be to pass the value of i explicitly , to be sure that each goroutine gets a different value of i. Please note that even in this case you are not deterrming the order of goroutines execution, but just making sure that each goroutine has a different value of i.

And this is all about goroutines , and some race conditions. I have not covered a lot of ways to help with these race conditions, let us take this a practice to play with goroutines and solve potential race conditinos before hand.
Will be back with the next article of the series covering channels.
In the meanwhile, you may like to read some articles about golang I posted here.

I consistently write about Golang and System Design, follow me on twitter and linkedIn to stay connected.


0 Comments

Leave a Reply

Avatar placeholder

Your email address will not be published. Required fields are marked *

Wordpress Social Share Plugin powered by Ultimatelysocial
error

Enjoy this blog? Please spread the word :)