S1E1: Concurrency In Go | Goroutine | Channels | Waitgroup

codepiper

CodePiper

Posted on July 23, 2023

S1E1: Concurrency In Go | Goroutine | Channels | Waitgroup

I am happy to announce that I have started a new series on Concurrency Design patterns with the working code. This playlist will contain 9-10 coding + concept videos. For full detail explanation please go to the CodePiper Youtube channel video: https://youtu.be/eTJ7P0RCZJc


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
}

Enter fullscreen mode Exit fullscreen mode

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")
}

Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

Youtube Channel: https://www.youtube.com/@codepiper
Github Link: https://github.com/arshad404/CodePiper/tree/main/GO/concurrency-patterns/Basics

💖 💪 🙅 🚩
codepiper
CodePiper

Posted on July 23, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related