Go Routines ๐Ÿงต

Concurrency, Routines, Critical Sections, Mutex, Wait Groups and Channels

ยท

7 min read

Go Routines ๐Ÿงต

Contents

Concurrency

Imagine you're watching the Cricket World Cup ๐Ÿ You're deeply engrossed in the game, and then the hunger strikes. So, you take a break to order a Domino's pizza๐Ÿ• and grab some orange juice๐ŸŠ๐Ÿฅค from the kitchen. Once the order is placed, you're back to enjoying the match.

๐Ÿ’ก What can we learn from this scenario?

In this situation, you had different tasks: watching TV, ordering pizza, and enjoying juice. However, you tackled them one at a time. While watching the match, you didn't want to go to the kitchen to grab juice or call to order a pizza ( you don't want to miss Virat's batting ;)

Now, let's think about it differently. Imagine two friends are with you, all passionate about the game. One friend orders pizza๐Ÿ•, and the other gets some juice๐Ÿงƒ๐Ÿฅค. Think of you and your friends as a team, each doing their part independently to make sure everyone enjoys the game. It's like everyone has a role, but the common goal is to have a great time watching cricket together.

Exactly that's what is CONCURRENCY !!
Concurrency is about efficiently handling multiple tasks and ensuring they can make progress independently when possible.

Routines

Go Routines are lightweight threads ๐Ÿงต that are used to achieve concurrency. They are functions that run concurrently with other functions in the program. Goroutines are managed by the Go runtime, which schedules them on available threads.

Without Routines

package main
import (
    "fmt"
    "time"
)

func takesTime(name string) {
    time.Sleep(time.Second * 1)
    fmt.Println("Hello : ", name)
}

func main() {
    start := time.Now()
    takesTime("A")
    takesTime("B")
    takesTime("C")
    takesTime("D")
    time.Sleep(time.Second * 2)

    fmt.Println(time.Since(start))
}

Command:
$ go run main.go

โœ… Using Routines

package main
import (
    "fmt"
    "time"
)

func takesTime(name string) {
    time.Sleep(time.Second)
    fmt.Println("Hello : ", name)
}

func main() {
    start := time.Now()
    go takesTime("A")
    go takesTime("B")
    go takesTime("C")
    go takesTime("D")
    time.Sleep(time.Second * 2)

    fmt.Println(time.Since(start))
}

Output:

What exactly happened here๐Ÿค”๐Ÿ’ก?

The first program don't use Go Routines. The code is executed synchronously. Time.Sleep(time.Second * 1) implies the function will pause execution for 1 second. Time.Sleep(time.Second * 2) enforces the main function to wait for 2 seconds allowing the routine to finish its task.

Total time without Go-Routine = 4 * 1 + 2 = 6s.

The go keyword is used to create a Go-Routine. It forks the main function and starts up a thread that executes the TakesTime function. Here 4 threads are created that run concurrently.

Total Time taken using Go-Routine = 2s.

Applications โฑ๏ธ๐Ÿš€โš–๏ธ๐Ÿ”„

  1. Web servers: Handling multiple client requests concurrently to improve server performance and responsiveness.

  2. Data processing pipelines: Efficiently processing and transforming data from various sources in parallel.

  3. Distributed systems: Coordinating tasks and communication in distributed systems and clusters.

  4. Real-time systems: Handling multiple tasks simultaneously with low-latency requirements, such as robotics and game engines.

  5. Load balancing: Distributing incoming requests across multiple worker goroutines to evenly distribute the workload.

Critical Section, Mutex & Wait Groups

Critical Section ๐Ÿ”’ ๐Ÿšง โš™๏ธ

A Critical Section refers to a portion of a program or code that has some resources available that are being shared by multiple processes and also it is designed to be executed by only one process or thread at a time. The purpose of defining a critical section is to ensure that Shared Resources (such as variables, data structures, or devices) are accessed in a mutually exclusive manner to prevent data corruption or race conditions.

Example: Booking Airplane tickets โœˆ๏ธ in the Air India App. Let's imagine two users are trying to book a seat with A01 number at the same time. They click on the seat number at the same time, but only one of them can book it. That's what is the critical section. At one time, only 1 process/thread should be allowed to access the critical section, otherwise, it creates inconsistency and chaos.

Mutex ๐Ÿ”’ ๐Ÿ” ๐Ÿ›ก๏ธ

Mutex is short for Mutual Exclusion. It is typically a data structure that provides exclusive access to a shared resource. A mutex can be locked by one thread or process at a time. If another thread or process attempts to lock the same mutex while it's already locked, it will be blocked or put into a waiting state until the mutex is unlocked by the owning thread.

Wait Group

WaitGroup is a synchronization primitive provided by the standard library for managing concurrent tasks and coordinating the completion of goroutines.

  1. Add: It is used to increment the counter by a specified value. You typically call Add before starting a goroutine, indicating how many goroutines you expect to wait for. For example, if you are launching three goroutines, you would call wg.Add(3).

  2. Done: It's called by each goroutine when it has completed its work. It decrements the counter by one, indicating that one goroutine has finished. One should call this method at the end of goroutine's execution using defer to ensure that the counter is decremented even if the goroutine encounters an error or panics.

  3. Wait: This method is used by the main goroutine to block until the counter reaches zero. When the counter reaches zero (i.e., all Done calls have been made), the Wait call unblocks, and the program can continue its execution.

package main

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

var (
    ticketsAvailable = 5
    mu               sync.Mutex
)

func bookTicket() {
    mu.Lock()
    defer mu.Unlock()

    if ticketsAvailable > 0 {
        time.Sleep(100 * time.Millisecond)

        // Book a ticket
        ticketsAvailable--
        fmt.Printf("Ticket booked. %d ticket(s) remaining.\n", ticketsAvailable)
    } else {
        fmt.Println("Sorry, no more tickets available.")
    }
}

func main() {
    var wg sync.WaitGroup
    numThreads := 5

    for i := 1; i <= numThreads + 1; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            fmt.Printf("Thread %d trying to book a ticket...\n", id)
            bookTicket()
        }(i)
    }

    wg.Wait()
}

Explanation:

  1. BookTicket Function: This function is where the magic happens. It's protected by the mutex, which means only one person can enter this function at a time. Inside the function, if there are tickets available, we simulate a short booking process (using time.Sleep to represent some time spent) and then decrement the number of available tickets. If there are no tickets left, we simply print a message indicating that the tickets are sold out.

  2. Main Function: In the main function, we create five threads (or "people") who want to book tickets. We use a sync.WaitGroup to ensure that we wait for all these threads to finish before the program exits.

  3. Concurrency in Action: Each thread tries to book a ticket by calling the bookTicket function. You'll see messages indicating which thread is trying to book a ticket. Thanks to the mutex, even though multiple threads are trying to book tickets simultaneously, only one at a time can actually access the bookTicket function.

  4. Outcome: As you run the program, you'll see the threads in action. If there are tickets left, they will be booked one by one. If all tickets are sold out, the program will politely inform you.

This code exemplifies how Go allows us to safely manage shared resources, such as tickets, in a concurrent environment. It's a simplified model of how real-world systems manage access to limited resources, like tickets for popular events. By using mutexes, we ensure that no more tickets are sold than are available, preventing any mishaps and ensuring a smooth ticket booking process for all. ๐ŸŽŸ๏ธ๐Ÿš€

Channels

Channels are a fundamental feature for facilitating communication and synchronization between goroutines.

package main
import (
    "fmt"
    "sync"
)
var wg sync.WaitGroup

func main() {
    // A Buffered channel with a capacity of 2
    messageChannel := make(chan string, 2)
    wg.Add(2)

    // 1st goroutine sends a message
    go func() {
        fmt.Println("")
        defer wg.Done()
        messageChannel <- "Hello from Goroutine 1!"
        fmt.Println("Goroutine 1 sent a message.")
    }()

    // 2nd goroutine receives and prints the message
    go func() {
        defer wg.Done()
        message := <-messageChannel
        fmt.Println("Goroutine 2 received:", message)
    }()

    wg.Wait()
}

  1. We create a buffered channel called messageChannel with a capacity of 2. The capacity determines how many messages can be stored in the channel without blocking. In this case, it can hold two messages.

  2. The first goroutine sends a message to the channel. It's "Hello from Goroutine 1!" in this example.

  3. The second goroutine receives the message from the channel and prints it.

Hope you learned something new. Any and all suggestions are welcomed. If you enjoyed the blog, consider sharing it with your friends :)

Free and customizable thank you templates

ย