siGithub siDiscord siTwitter
golang

Concurrency Management in Golang Exploring Mutexes and Channels

Muhammad Asghar Ali

Svelte

Before understanding these two concepts we need to understand goroutines first. A goroutine is an independent function that executes simultaneously in some separate lightweight threads managed by Go.

For example:

package main
import (
  "fmt"
  "time"
)

// create a function
func displayMessage(message string) {
  fmt.Println(message)
}

func main() {
  // call goroutine
  go displayMessage("From process 1")

  // do other work
}

The displayMessage() method in a new goroutine is run concurrently in the example above. The main programme doesn’t have to wait for displayMessage() to finish before executing additional code.

Data Synchronization and Sharing

In Golang, goroutines share the same memory space, allowing them to share data. However, this might cause synchronization problems because many goroutines may try to modify the same data at the same time.

To prevent this, Golang has synchronization primitives like mutexes and channels. Both mutexes and channels play roles in managing concurrency, but they serve different purposes.

Channels

Channels are the pipes that connect concurrent goroutines. You can send values into channels from one goroutine and receive those values into another goroutine. Channels can also be used to synchronize goroutines by signaling when a particular operation is completed.

package main

import (
 "fmt"
 "time"
)

func producer(ch chan<- int) {
 for i := 0; i < 5; i++ {
  fmt.Println("Producing:", i)
  ch <- i                            // Send data to the channel
  time.Sleep(time.Millisecond * 500) // Simulate some work
 }
 close(ch) // Close the channel when done producing
}

func consumer(ch <-chan int) {
 for {
  data, ok := <-ch // Receive data from the channel
  if !ok {
   fmt.Println("Channel closed. Exiting consumer.")
   return
  }
  fmt.Println("Consuming:", data)
 }
}

func main() {
 // Create an unbuffered channel
 dataChannel := make(chan int)

 // Start producer and consumer goroutines
 go producer(dataChannel)
 go consumer(dataChannel)

 // Let the main goroutine sleep to allow other goroutines to execute
 time.Sleep(time.Second * 3)
}

Output:

Svelte

The producer function sends integers to the channel (ch) in a loop. The channel is passed as an argument with the chan<- int syntax, indicating it’s a send-only channel.

The consumer function receives data from the channel (ch) in an infinite loop. The channel is passed as an argument with the <-chan int syntax, indicating it’s a receive-only channel. The loop continues until the channel is closed.

In the main function, an unbuffered channel (dataChannel) is created. The producer and consumer goroutines are started concurrently.

The main goroutine sleeps for a while to allow the producer and consumer to execute. When the program is finished, the main goroutine exits, and the remaining goroutines will also exit since the dataChannel is closed.

Mutex

A mutex (short for mutual exclusion) is a synchronization primitive used to protect shared data from simultaneous access by multiple goroutines. It helps to avoid race conditions where two or more goroutines may attempt to modify shared data concurrently, leading to unpredictable behavior.

For example:

package main

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

var counter = 0
var mutex sync.Mutex

func increment() {
 for i := 0; i < 5; i++ {
  mutex.Lock()
  counter++
  fmt.Println("Incrementing counter to:", counter)
  mutex.Unlock()
  time.Sleep(time.Millisecond * 500) // Simulate some work
 }
}

func main() {
 // Start two goroutines that increment the counter
 go increment()
 go increment()

 // Let the main goroutine sleep to allow other goroutines to execute
 time.Sleep(time.Second * 3)

 fmt.Println("Final counter value:", counter)
}

Output:

Svelte

The counter variable is a shared resource protected by a mutex.

The increment() function increments the count variable and uses the mutex.Lock() and mutex.Unlock() methods to ensure that the variable is being accessed by only one goroutine at a time.

In the main function, two goroutines are started concurrently without using sync.WaitGroup.

The main goroutine sleeps for a while to allow other goroutines to execute.

The final value of the counter is printed.

Conclusion

Goroutines, a key feature in the Go programming language, empower developers to create efficient and resilient concurrent programs. With goroutines, code can execute multiple tasks concurrently without requiring intricate locking or synchronization mechanisms. Nevertheless, it is crucial to recognize potential synchronization challenges when sharing data between goroutines, prompting the use of synchronization tools like mutexes and channels to mitigate such issues.

Use channels when you need to facilitate communication and coordination between goroutines. Channels are especially useful for scenarios where data needs to be passed between goroutines or when signaling is required.

Use mutexes when you need to protect shared data from concurrent modifications. Mutexes are essential for scenarios where multiple goroutines may access and modify shared resources, and you want to ensure data integrity.

With durable delivery, you focus on what matters most. Your customers!

Leave the complexity of distributed system and cloud to us as we you help you achieve high availibility and high performance.

Join the Waitlist!

Enter your company email.
Your full name.
This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.