S1E1: Mastering Concurrency In Go | Goroutine | Channels | Waitgroup | Buffered Channel

Arshad
5 min readJul 29, 2023

--

In this series we have already learned: (Concurrency Design Patterns — YouTube)

  1. S1E1: Concurrency In Go | Goroutine | Channels | Waitgroup | Buffered Channel | by Arshlan | Jul, 2023 | Medium
  2. S1E2: Concurrency Boring Desing Pattern in Go | by Arshlan | Jul, 2023 | Medium
  3. S1E3: Mastering Concurrency with Worker Pool in GoLang: A Scalable Solution for Efficient Task Processing | by Arshlan | Aug, 2023 | Medium
  4. S1E4: Mastering Concurrency Fan-In Design Pattern | by Arshlan | Aug, 2023 | Medium
  5. S1E5: Mastering the Concurrency in Go with Fan Out Design Pattern | by Arshlan | Aug, 2023 | Medium

I am happy to announce that I have started a new series on Concurrency Design patterns with the working code. In this blog I will explain the basics of concurrency. For full detail explanation please go to the CodePiper Youtube channel video: S1E1: Concurrency Concepts In GO | goroutines | channels | waitgroup | deadlock — YouTube

This blog will cover
1. Goroutines
2. Waitgroup
3. Channel
4. Buffered Channel
5. Deadlock in concurrency

Goroutine:
Goroutines are the building blocks of concurrent programming in Go (often referred to as Golang). They are lightweight, independently executing functions that run concurrently with other goroutines in the same address space. Goroutines are managed by the Go runtime, and they utilize the available CPU cores efficiently through a technique called “multiplexing.” You can think of a goroutine as a function that runs independently and concurrently with other functions, allowing for efficient and scalable concurrency in Go programs.

func printNumbers() {
for i := 1; i <= 5; i++ {
fmt.Printf("%d ", i)
time.Sleep(100 * time.Millisecond)
}
}

func printLetters() {
for ch := 'a'; ch <= 'e'; ch++ {
fmt.Printf("%c ", ch)
time.Sleep(100 * time.Millisecond)
}
}

func main() {
go printNumbers() // Start a new Goroutine for printNumbers()
printLetters() // Execute printLetters() concurrently with the Goroutine
}

WaitGroup:
WaitGroup is a synchronization primitive provided by the Go standard library in the sync package. It is used to wait for a collection of goroutines to complete their execution before proceeding further in the main goroutine. A WaitGroup is essentially a counter that is incremented when a new goroutine is started and decremented when a goroutine finishes its task. The main goroutine can call Wait on the WaitGroup, which will block until the counter becomes zero (i.e., all goroutines have completed their tasks and called Done on the WaitGroup). It helps in coordinating the termination of multiple goroutines.

func printNumbers(wg *sync.WaitGroup) {
defer wg.Done() // Notify WaitGroup that this Goroutine is done when the function returns
for i := 1; i <= 5; i++ {
fmt.Printf("%d ", i)
time.Sleep(100 * time.Millisecond)
}
}

func printLetters(wg *sync.WaitGroup) {
defer wg.Done() // Notify WaitGroup that this Goroutine is done when the function returns
for ch := 'a'; ch <= 'e'; ch++ {
fmt.Printf("%c ", ch)
time.Sleep(100 * time.Millisecond)
}
}

func main() {
var wg sync.WaitGroup
wg.Add(2) // Add the number of Goroutines to wait for

go printNumbers(&wg) // Start a new Goroutine for printNumbers()
go printLetters(&wg) // Start a new Goroutine for printLetters()

wg.Wait() // Wait until all Goroutines in the WaitGroup finish
fmt.Print("Okay you can go")
}

Channel:
A channel in Go is a typed conduit that allows communication and synchronization between different goroutines. It is a way for goroutines to send and receive values to and from each other. Channels provide a safe way to exchange data and communicate between goroutines, preventing race conditions and other concurrency-related issues. Channels can be used to pass data from one goroutine to another, enabling the coordination of concurrent tasks effectively. Channels can be unbuffered or buffered, depending on their capacity.

func sender(ch chan<- int) {
for i := 1; i <= 10; i++ {
ch <- i // Send data to the channel
}
close(ch) // Close the channel after sending all data
}

func receiver(ch <-chan int) {
for num := range ch {
fmt.Printf("%d ", num) // Receive data from the channel
}
}

func main() {
ch := make(chan int)

go sender(ch) // Start a new Goroutine for the sender
receiver(ch) // Execute the receiver concurrently with the Goroutine
}

Buffered Channel:
A buffered channel is a type of channel in Go that has a fixed capacity to hold a certain number of elements. When you create a buffered channel, you specify the capacity as the second argument to the make function. For example, ch := make(chan int, 10). In this case, the channel ch can hold up to 10 elements. When a goroutine sends a value to a buffered channel, it will be added to the channel’s internal buffer if there is space available. If the channel is full, the sending goroutine will block until there is room in the buffer. Similarly, when a goroutine tries to receive a value from a buffered channel, it will receive data from the buffer if it’s not empty. If the buffer is empty, the receiving goroutine will block until data is available.

func main() {
ch := make(chan int, 3) // Create a buffered channel with a capacity of 3

ch <- 1 // Send data to the channel
ch <- 2 // Send more data
ch <- 3 // Send even more data

// ch <- 4 // Sending a 4th value would cause a deadlock because the channel is full

fmt.Println(<-ch) // Receive data from the channel
fmt.Println(<-ch) // Receive more data
fmt.Println(<-ch) // Receive even more data

// fmt.Println(<-ch) // Receiving a 4th value would cause a deadlock because the channel is empty
}

NOTE: Using buffered channels can sometimes improve the performance of concurrent programs by reducing contention between goroutines, as long as the buffer size is chosen wisely based on the specific use case. However, it’s essential to be mindful of buffer sizes to avoid consuming excessive memory.

func producer(ch chan<- int) {
for i := 1; i <= 5; i++ {
fmt.Printf("Producing %d\n", i)
ch <- i
time.Sleep(500 * time.Millisecond) // Simulate some work by the producer
}
close(ch)
}

func consumer(ch <-chan int) {
for num := range ch {
fmt.Printf("Consuming %d\n", num)
time.Sleep(1 * time.Second) // Simulate some work by the consumer
}
}

func main() {
normalCh := make(chan int) // Normal channel
bufferedCh := make(chan int, 3) // Buffered channel with capacity 3

fmt.Println("Using Normal Channel:")
go producer(normalCh)
consumer(normalCh)

time.Sleep(2 * time.Second) // Wait for the producer to finish

fmt.Println("\nUsing Buffered Channel:")
go producer(bufferedCh)
consumer(bufferedCh)
}
Using Normal Channel:
Producing 1
Consuming 1
Producing 2
Consuming 2
Producing 3
Consuming 3
Producing 4
Consuming 4
Producing 5
Consuming 5

Using Buffered Channel:
Producing 1
Consuming 1
Producing 2
Consuming 2
Producing 3
Producing 4
Consuming 3
Producing 5
Consuming 4
Consuming 5

--

--

Arshad
Arshad

No responses yet