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
.
-
Send: Sending a value to a channel using the syntax
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
}
The program functions as follows:
What I Learned
-
Using Goroutines with the
go
Keyword: Thego
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 andwg.Done
decrements it, andwg.Wait
blocks until the counter reaches zero. - Investigate additional functionalities and use cases of
WaitGroup
.
- Understanding that
-
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.
- Learned about the
Top comments (1)
A WaitGroup provides three primary methods:
Add(delta int)
,Done()
, andWait()
. In this example, I have utilized all three methods to manage the synchronization of goroutines.