DEV Community

Usk
Usk

Posted on • Edited on

2 1 1 1

Concurrency, Goroutines, and Channels in Go: A Study

Abstract

This article explores the fundamental concepts of concurrency in Go, focusing on Goroutines and Channels.

  • Concurrency: The ability to handle multiple tasks in overlapping time periods. It does not imply that tasks are executed simultaneously, but rather that they are managed in a way that allows progress on multiple tasks within a given timeframe.
  • Goroutine: The smallest unit of execution in Go, enabling concurrent operations. Goroutines are lightweight threads managed by the Go runtime and can be created using the go keyword.
  • Channel: A conduit for communication and synchronization between Goroutines. Channels allow Goroutines to send and receive values of a specified type.
    • Send: Sending a value to a channel using the syntax ch <- value.
    • Receive: Receiving a value from a channel using the syntax value := <-ch.

Implementing a Simple Example Similar to Docker Pull

package main

import (
    "fmt"
    "math/rand"
    "os"
    "strconv"
    "sync"
    "time"
)


func main() {
    // Create random seed
    rand.Seed(time.Now().UnixNano())

    // Set concurrency count
    if len(os.Args) < 2 {
        fmt.Println("Usage: go run main.go [number]")
        return
    }
    n, err := strconv.Atoi(os.Args[1])
    if err != nil || n <= 0 {
        fmt.Println("Invalid number. Please provide a positive integer.")
        return
    }

    // A slice use to track the progress of each goroutine. (1 - 100)
    progress := make([]int, n)

    // A mutex to prevent conflicts during progress updates.
    var mu sync.Mutex

    // WaitGroup for waiting until each goroutine has completed
    var wg sync.WaitGroup
    wg.Add(n)

    // Create n goroutine
    for i := 0; i < n; i++ {
        go func(idx int) {
            defer wg.Done()
            // Each goroutine will update its progress until it reaches 100%
            for {
                // The loop breaks if progress reaches 100%
                mu.Lock()
                if progress[idx] >= 100 {
                    mu.Unlock()
                    break
                }
                // Add a random progress value (1-10%)
                inc := rand.Intn(10) + 1
                progress[idx] += inc
                if progress[idx] > 100 {
                    progress[idx] = 100
                }
                mu.Unlock()

                // Sleep for a random duration between 100 and 500 milliseconds
                time.Sleep(time.Duration(rand.Intn(400)+100) * time.Millisecond)
            }
        }(i)
    }

    // A channel to signal that all goroutines have finished
    // The channel uses struct{} as a signal. The actual data type is irrelevant since only the signal is needed.
    done := make(chan struct{})
    go func() {
        wg.Wait() // Wait until all wg(WaitGroup) are done.
        done <- struct{}{} // And send data to done channel. This channel is referenced by select statement below.
    }()

    // Periodically draw the progress status on the screen
    ticker := time.NewTicker(100 * time.Millisecond)
    defer ticker.Stop()

    for {
        select {
        case <-ticker.C:
            // Clear the screen and redraw
            printProgress(progress, &mu)
        case <-done:
            // Display the final state and exit
            printProgress(progress, &mu)
            return
        }
    }
}

// printProgress is a function that displays the progress of each indicator
// Prevent concurrent access to progress data using a mutex
func printProgress(progress []int, mu *sync.Mutex) {
    mu.Lock()
    defer mu.Unlock()

    // Clear the screen using ANSI escape sequences
    // \033[H : Move the cursor to the home position
    // \033[2J : Clear the screen
    fmt.Print("\033[H\033[2J")
    for i, p := range progress {
        // Set the total width of the progress bar to 50 characters
        width := 50
        // Number of "*" characters based on progress percentage
        stars := p * width / 100
        spaces := width - stars
        fmt.Printf("%d.[%s%s] %d%%\n", i+1, repeat("*", stars), repeat(" ", spaces), p)
    }
}

// repeat: Returns concatenated string by repeating string "s" "count" times
func repeat(s string, count int) string {
    result := ""
    for i := 0; i < count; i++ {
        result += s
    }
    return result
}
Enter fullscreen mode Exit fullscreen mode

The program functions as follows:

Program Demonstration

What I Learned

  • Using Goroutines with the go Keyword: The go keyword allows the creation of goroutines, enabling concurrent execution of functions.
  • Concurrency with Goroutines: Goroutines facilitate concurrent processing, as demonstrated by managing the progress of multiple indicators simultaneously in this example.

Questions and Future Investigations

  • WaitGroup:
    • Understanding that wg.Add increments the counter and wg.Done decrements it, and wg.Wait blocks until the counter reaches zero.
    • Investigate additional functionalities and use cases of WaitGroup.
  • Mutex:
    • Understanding that mutexes prevent race conditions by ensuring exclusive access to shared resources during updates.
    • Curious about the specific issues that arise when mutexes are not used, such as inconsistent variable states.
    • Interested in intentionally creating race conditions to observe and understand their effects.
  • Testing Methods:
    • Learned about the go test -race flag for detecting race conditions.
    • Plan to explore this testing method further to ensure code safety and reliability.

AWS Security LIVE!

Tune in for AWS Security LIVE!

Join AWS Security LIVE! for expert insights and actionable tips to protect your organization and keep security teams prepared.

Learn More

Top comments (1)

Collapse
 
uskdev profile image
Usk

Investigate additional functionalities and use cases of WaitGroup.

A WaitGroup provides three primary methods: Add(delta int), Done(), and Wait(). In this example, I have utilized all three methods to manage the synchronization of goroutines.

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more