A Practical Guide to Concurrency in Golang — Key Terms and Examples

Understand concurrency in Golang
Feb 15 2023 · 7 min read

Background

When we search term concurrency in Golang, we come around mainly to the goroutines and channels. Sometimes it's waitgroup or mutex. There are many terms that can explain concurrency in Golang. But we get confused with all things when we start implementing the same.

The same thing happened to me and as an outcome came up with this blog.

Today we will discuss concurrency in Golang with an example of sentiment data processing using goroutines, channels, waitgroups, and many more terms.

Sentiment data processing: It is a data processing example, in which we can analyze sentiment data from social media posts, comments, or any expression-related data, whether the data is positive, neutral, or negative.
 

Good concurrent habits take the ground in improving efficiency in life and work. Justly will aid your goals. Try it out today.

Introduction

Concurrency in Go is one of the language’s key features and has been designed to make it easy to write highly concurrent and scalable applications.
It is the ability to execute multiple tasks or processes simultaneously, that allows efficient use of system resources and improves application performance.

Concurrency vs parallelism with an example

Note that concurrency and parallelism are two different terms.

  • Concurrency — It’s meant to domultiple tasks at the same time. It is a term used for writing programs.
  • Parallelism —It’s meant to domultiple tasks with multiple resources at the same time . It is hardware related term, used when building CPUs and cores.

Example — Students have been assigned a task to write a paragraph from the board.

  • Concurrency: Students are reading from the board and writing at the same time. (one resource — student)
  • Parallelism: The teacher is reading loudly and students are writing at the same time. (multiple resources — student & teacher)

Let’s start understanding all the terms step by step with the sentiment data processing example.

Goroutines and channels

Goroutines and channels are mainly used for achieving concurrency in Golang.

Goroutines

Goroutines can be thought of as functions that run independently in the background, allowing the program to perform multiple tasks simultaneously.

A goroutine is created using the go keyword followed by a function call. The function is then executed concurrently in its own goroutine, allowing other parts of the program to continue running without waiting for the function to complete.

Channels

Channels are an essential part of concurrency in Go, allowing Goroutines to communicate with each other and share data.

A channel is a medium through which you can send and receive values with the channel operator, <-. This operator can either send or receive data, depending on the side of the channel on which it is used.

Example

package main

import (
 "fmt"
 "strings"
)

func analyzeSentiment(data string, resultChan chan string) {
 // Perform sentiment analysis on the input data
 // Here, we simply check if the input contains the word "happy"
 if strings.Contains(strings.ToLower(data), "happy") {
  resultChan <- "positive"
 } else {
  resultChan <- "negative"
 }
}

func main() {
 // Define the input data
 input := []string{
  "I am so happy today!",
  "I hate this weather.",
  "Happy birthday!!",
 }

 // Create a channel for the sentiment analysis results
 resultChan := make(chan string)

 // Launch a goroutine to analyze the sentiment of each input string
 for _, data := range input {
  go analyzeSentiment(data, resultChan)
 }

 // Wait for the results to be processed and print them
 for i := 0; i < len(input); i++ {
  fmt.Println(<-resultChan)
 }
}

In the above code,

  • Created channels resultChan to store the results of sentiment analysis.
  • analyzeSentiment is a simple method to analyze sentiment data. This is a basic example. More algorithms and theories can be used to analyze real-time data.
  • We have used the goroutine go analyzeSentiment() to process input data concurrently.

Try out this example using more input data and observe the result.

Output

It can be different for your case because,

  • Multiple threads or processes are executing instructions simultaneously
  • The execution order can vary depending on factors such as the operating system’s scheduler, the workload on the system, and the specifics of the program’s implementation.
     
positive
positive
negative

Waitgroups

We have defined goroutines, which will work in the background, but how would we come to know when these goroutines will be completed?

Without a waitgroup, there is no way to know when multiple Goroutines have been completed, which can lead to unexpected results, race conditions, and other issues.

Waitgroup is a primitive of Go’s sync package. They provide a way to synchronize the execution of Goroutines and ensure that all Goroutines have been completed before terminating.

It increases the count by 1 when goroutines start executing and decrease it when it completes. Therefore, the main goroutine will wait until the count reaches 0 and then continue to the next execution.

Example

package main

import (
 "fmt"
 "strings"
 "sync"
)

var wg sync.WaitGroup // Initialize waitgroup

func analyzeSentiment(data string, resultChan chan string) {
 // Signal that the goroutine has completed its work
 defer wg.Done()

 if strings.Contains(strings.ToLower(data), "happy") {
  resultChan <- "positive"
 } else {
  resultChan <- "negative"
 }
}

func main() {
 input := []string{
  "I am so happy today!",
  "I hate this weather.",
  "Happy birthday!!",
 }

 resultChan := make(chan string)

 for _, data := range input {
  // Add one to the waitgroup for each goroutine
  wg.Add(1)
  go analyzeSentiment(data, resultChan)
 }

 go func() {
  // Wait for all goroutines to complete
  wg.Wait()
  // Close the result channel to signal the workers to terminate
  close(resultChan)
 }()

 // Print the results
 for i := 0; i < len(input); i++ {
  fmt.Println(<-resultChan)
 }
}

In the above code,

  • Initiate the waitgroup using var wg sync.WaitGroup
  • Increase count when starting the goroutine go analyzeSentiment .
  • Defined another goroutine that will wait until all other goroutines will be completed and closed the channel to ensure that all the data has been received.
  • Added wg.Done in analyzeSentiment to signal that the goroutine has been completed.

Mutex

During concurrent execution, code may enter the critical section(resource or code accessed by multiple processes). When the values of the critical section depend on the sequence of execution(i.e increasing counter), then it becomes inconsistent and results in the race conditions.

To avoid this, Mutex(mutual exclusion) can be used. It is working on a locking mechanism. When a resource is acquired by one process, add a lock, and after finishing it, unlock it.

For example, In clothes store, when one person is using trial room, he/she will lock the room, try the cloth and unlock the room, other persons will wait till the room is unlocked. Here trial room is critical section.

It’s important to use mutexes carefully, as they can introduce overhead and potentially lead to deadlocks if used incorrectly. However, in situations where multiple goroutines need to access a shared resource, a mutex can be a useful tool for ensuring synchronization and avoiding race conditions.

Mutex is also provided by the sync package.

Example

package main

import (
 "fmt"
 "strings"
 "sync"
)

// Create a mutex to synchronize access to the counter variable
var mu sync.Mutex
var wg sync.WaitGroup

func analyzeSentiment(data string, resultChan chan string, counter *int) {

 defer wg.Done()

 if strings.Contains(strings.ToLower(data), "happy") {
  // Acquire the lock before accessing the shared counter variable
  mu.Lock()
  *counter++
  mu.Unlock()
  resultChan <- "positive"
 } else {
  resultChan <- "negative"
 }
}

func main() {
 input := []string{
  "I am so happy today!",
  "I hate this weather.",
  "Happy birthday!!",
 }

 resultChan := make(chan string)

 // Create a counter variable to track the number of positive sentiments
 counter := 0

 for _, data := range input {
  wg.Add(1)
  go analyzeSentiment(data, resultChan, &counter)
 }
 
 go func() {
  wg.Wait()
  close(resultChan)
 }()

 // Print the results
 for i := 0; i < len(input); i++ {
  fmt.Println(<-resultChan)
 }

 // Print the number of positive sentiments
 fmt.Printf("%d out of %d input strings had a positive sentiment.\n", counter, len(input))
}

In the above code,

  • Initiate the mutex using var mu sync.Mutex
  • Add a counter to count positive results and pass it to analyzeSentiment.
  • In analyzeSentiment , used mu.Lock() to acquire the lock while increasing the counter and releasing the lock with mu.Unlock()
  • It will help goroutines to increase the counter in synchronization.

Output

positive
negative
positive
2 out of 3 input strings had a positive sentiment.

Workers

A worker is a goroutine that performs a specific task or set of tasks in the background, independently of the main program or other workers.

The basic idea behind workers is to create a pool of goroutines that can be used to perform a set of tasks concurrently. By using them, you can achieve parallelism because workers will take advantage of the available system resources and achieve higher performance and throughput.

Example

package main

import (
 "bufio"
 "fmt"
 "os"
 "strings"
 "sync"
)

var mu sync.Mutex
var wg sync.WaitGroup

func analyzeSentiment(data string, resultChan chan<- string) {
 if strings.Contains(strings.ToLower(data), "happy"){
  resultChan <- "positive"
 } else {
  resultChan <- "negative"
 }
}

func worker(inputChan <-chan string, resultChan chan<- string, k int) {

 defer wg.Done()

 for data := range inputChan {
  analyzeSentiment(data, resultChan)

  // Acquire the lock to access worker
  mu.Lock()
  fmt.Printf("Worker %d processed line: %s\n", k, data)
  mu.Unlock()
 }

}

func main() {
 inputChan := make(chan string, 10)
 resultChan := make(chan string, 10)

 // Launch two worker goroutines to process the sentiment analysis results
 for i := 0; i < 2; i++ {
  wg.Add(1)
  go worker(inputChan, resultChan, i)
 }

 // Read lines from stdin and send them to the workers
 scanner := bufio.NewScanner(os.Stdin)
 for scanner.Scan() {
  line := scanner.Text()
  inputChan <- line
 }
 close(inputChan)

 go func() {
  wg.Wait()
  close(resultChan)
 }()

 numPositive := 0
 numNegative := 0
 for result := range resultChan {
  switch result {
  case "positive":
   numPositive++
  case "negative":
   numNegative++
  }
 }

 // Print the results
 fmt.Printf("Positive: %d\n", numPositive)
 fmt.Printf("Negative: %d\n", numNegative)
}

In the above code, I had taken input from stdin to analyze sentiment data for efficient use of workers.

  • Created inputChan to store input data in it.
  • Taken 2 workers which will work on inputChan and analyze data and store results in resultChan.
  • After finishing with lines, press ctrl+D and you will get positive and negative counts.

Output

I am so happy today!
Worker 0 processed line: I am so happy today!
I hate this weather.
Worker 1 processed line: I hate this weather.
Happy birthday!!
Worker 0 processed line: Happy birthday!!
Positive: 2
Negative: 1

Conclusion

Golang has rich support for concurrency using the goroutines and channels. In addition, Go provides some default primitives like Mutex, Waitgroups, Map, Once from the sync package, and timeouts and cancellations for controlling concurrent processes.

We’re Grateful to have you with us on this journey!

Suggestions and feedback are more than welcome! 

Please reach us at Canopas Twitter handle @canopas_eng with your content or feedback. Your input enriches our content and fuels our motivation to create more valuable and informative articles for you.

And with that, we’ll wrap things up for today. Keep learning.


sumita-k image
Sumita Kevat
Sumita is an experienced software developer with 5+ years in web development. Proficient in front-end and back-end technologies for creating scalable and efficient web applications. Passionate about staying current with emerging technologies to deliver.


sumita-k image
Sumita Kevat
Sumita is an experienced software developer with 5+ years in web development. Proficient in front-end and back-end technologies for creating scalable and efficient web applications. Passionate about staying current with emerging technologies to deliver.

canopas-logo
We build products that customers can't help but love!
Get in touch

Let's Work Together

Not sure where to start? We also offer code and architecture reviews, strategic planning, and more.

cta-image
Get Free Consultation
footer
Subscribe Here!
Follow us on
2024 Canopas Software LLP. All rights reserved.