Introduction to Hybrid Concurrency in Go
Go (or Golang) is a language that has become incredibly popular for building concurrent software. Its concurrency model is built around the concept of goroutines and channels. While goroutines allow you to run functions concurrently, channels are used for communication between these goroutines. This article explores how you can combine channels with mutexes to achieve a hybrid concurrency model.
Understanding Goroutines and Channels
Goroutines are functions or methods that run concurrently with other functions. Channels, on the other hand, are a way for goroutines to communicate.
package main
import (
"fmt"
"time"
)
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("world") // This runs as a goroutine
say("hello") // This runs in the main goroutine
}
Basic Channel Example
Here’s how you can make use of channels for communication between goroutines:
package main
import "fmt"
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // Send sum to channel c
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // Receive from channel c
fmt.Println(x, y, x+y)
}
Introducing Mutexes
A Mutex allows you to lock data so that only one goroutine at a time can access it. This is needed when you have shared resources that could be accessed and modified concurrently, leading to race conditions.
package main
import (
"fmt"
"sync"
)
var (
counter int
mu sync.Mutex
)
func increment(wg *sync.WaitGroup) {
defer wg.Done()
mu.Lock() // Lock the shared variable
counter++
mu.Unlock() // Unlock after incrementing
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go increment(&wg)
}
wg.Wait()
fmt.Println("Final Counter:", counter)
}
Combining Channels and Mutexes
Sometimes, you need both communication as well as protection of shared data. This is where the combination of channels and mutexes becomes powerful.
package main
import (
"fmt"
"sync"
)
func producer(c chan int, wg *sync.WaitGroup) {
defer wg.Done()
for i := 0; i < 5; i++ {
c <- i
}
}
func consumer(c chan int, results *[]int, mu *sync.Mutex, wg *sync.WaitGroup) {
defer wg.Done()
for i := 0; i < 5; i++ {
value := <-c
mu.Lock()
*results = append(*results, value)
mu.Unlock()
}
}
func main() {
c := make(chan int)
var wg sync.WaitGroup
var results []int
var mu sync.Mutex
wg.Add(1)
go producer(c, &wg)
wg.Add(1)
go consumer(c, &results, &mu, &wg)
wg.Wait()
fmt.Println("Results:", results)
}
Conclusion
By understanding how to effectively combine channels and mutexes, you can leverage the strengths of both concurrency models in Go. Channels enable goroutines to safely communicate with each other, while mutexes provide a straightforward solution for managing access to shared data. Using these tools together can significantly enhance the efficiency and safety of concurrent applications in Go.