Goroutines for Concurrency

GoLang (Go) is a programming language known for its concurrency and efficiency. And how does it provide such rich support for concurrency? Via Goroutines.
Goroutines are, as what we can compare to, lightweight threads that execute more than one task simultaneously. This article is about why we use Goroutines, and how we use them. Before going to Goroutines we need to look on what is concurrency.

 

Concurrency is not parallelism:

 

In programming, concurrency is the composition of independently executing processes, while parallelism is the simultaneous execution of (possibly related) computations. Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once.

 

Goroutines:

 

Any function or method in Go can be created as a goroutine. We can consider that the main function is executing as a goroutine (parent goroutine). Goroutines are considered to be lightweight because they use little memory and resources, plus their initial stack size is small. Prior to version 1.2, the stack size started at 4K and now as of version 1.4, it starts at 8K. The stack has the ability to grow as needed.

We can create a new goroutine by prefixing any function call with the keyword go. This creates a new goroutine containing the call frame and schedules it to run. The newly created goroutine behaves like a thread in other languages. It can access its arguments, any globals, and anything reachable from them.

 

Example goroutine:

 

package main

import ("fmt"
"time"
)

func main(){ // main goroutine

fmt.Printf("Goroutines Flow\n")

incrementVariable := 1

// spawned goroutine 1

go fmt.Printf("Currently, incrementVariable is %d\n", incrementVariable )

// spawned goroutine 2 with asynchronous function
go func (){

fmt.Printf("incrementVariable is: %d\n", incrementVariable)

}()

incrementVariable++

time.Sleep(1000000000)

}

 The time.Sleep() stops the main goroutine from terminating the program before the two spawned goroutines are completed, since Go does not require all goroutines to exit before the program terminates. The go triggers the goroutine and thereby gets executed independently.

 

The compiler does not have any constraints on the ordering of memory accesses from a concurrent goroutine, and so it is completely free to fold the increment into the initialization. This means that the line after the goroutine’s creation may actually run before.

 

Contrary to the above example, in real time, it is usually not possible to predict the execution time per process, and hence there needs to be some sort of communication between concurrencies. This is achieved by the technique of synchronization.

 

Synchronizing Goroutines:

 

Like in other programming languages we can synchronize two or more concurrent goroutines. The sync package provides mutexes. These are simple locks that can be held by at most one goroutine at a time. They provide two methods, Lock() and Unlock(), for acquiring and releasing the mutex.

 

For instance, when we perform operations on maps, they are not atomic, and so attempts to modify them concurrently will produce undefined behaviour. In such scenarios we need a synchronization between concurrencies. Example below:

 


package main

import "fmt"

import "sync"

func main() {

mapVariable := make( map [int] string)

mapVariable[0] = "i'm first"

var mutex sync.Mutex // create a mutex

conditionalVariable := sync.NewCond(&mutex) // conditional variable

updateCompleted := false

go func () {

conditionalVariable.L.Lock() // provide the lock

mapVariable[0] = "i'm second"

updateCompleted = true

conditionalVariable.Signal() // wakeup one gorotine

conditionalVariable.L.Unlock() // release the lock

}()

conditionalVariable.L.Lock()

for !updateCompleted {

conditionalVariable.Wait()

}

mapElement := mapVariable[0] conditionalVariable.L.Unlock()

fmt.Printf("%s\n", mapElement)

}

Wait() atomically unlocks conditional lock(c.L) and suspends execution of the calling goroutine. After later resuming execution, Wait() locks c.L before returning. Unlike in other systems, Wait() cannot return unless awoken by Broadcast() or Signal().

 

Because c.L is not locked when Wait() first resumes, the caller typically cannot assume that the condition is true when Wait() returns. Instead, the caller should Wait() in a loop. This avoids mentioning a time limit as in our previous example. Sync package provides the wait groups option which will stop the termination of program until completion of all goroutines.

 

Another method to synchronize is to keep a count of the number of mutexes used. This method does not involve Lock and Unlock. Instead, as in the below example, we count the number of mutexes (Add groups in this example) and wait for all of them to complete and finally terminate the program.

 


package main

import ("fmt"
"sync"
)

func main(){

var waitGroupVariable sync.WaitGroup

for incrementVariable := 0;incrementVariable<10;incrementVariable++ {

waitGroupVariable.Add(1)

go func (incrementVariable int) {

fmt.Printf("goroutine no: %v\n",incrementVariable)

waitGroupVariable.Done()

}(incrementVariable)

}

waitGroupVariable.Wait()

}

 

In the above example, the program will wait for completion of 10 goroutines which will be noted by waitGroupVariable.Add(), waitGroupVariable.Done()  and waitGroupVariable.Wait()Add will increase the goroutine count, Done notify the completed goroutines, Wait stops the termination of program until all goroutines are completed.

 

In this article, we have discussed the advantages, usage and synchronization of Goroutines. The possibilities with Goroutines are many and can be used across various streams including backend query processing, bulk data transmission, etc. In a nutshell, Goroutines can replace all processes which use threads for execution, and thereby define the efficiency of GoLang.