Golang (Go) is a programming language known for its concurrency and efficiency. And it is the fastest-growing programming language is backed by Google. Go provides rich support for concurrency via goroutines. This blog is will introduce the approach of concurrency in Golang.
Goroutines are lightweight threads that execute more than one task simultaneously. here we will take a look at how we can use Goroutines. Before getting into Goroutines we need to understand what is concurrency and how it differs from parallelism.
What are Concurrency and 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. A concurrent program has multiple logical threads. These threads may or may not run in parallel.
What are 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.
Goroutines – Example
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) }
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 Golang 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.
How to Synchronize 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 behavior. In such scenarios, we need synchronization between concurrencies.
Example
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() automatically 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 group option which will stop the termination of the 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" 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) }
In the above example, the program will wait for the completion of 10 goroutines which will be noted by waitGroupVariable.Add(), waitGroupVariable.Done() andwaitGroupVariable.Wait(). Add will increase the goroutine count, Done notify the completed goroutines, Wait for stops the termination of the 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.
We offer Golang development services for building world-class enterprise apps. We have expertise in building the most complex software solutions using Google’s Go language. Chat with us now and hire Golang developers within 72 hours.