Table of Contents
- Introduction to Concurrency
- Concurrency vs Parallelism
- Go-routines: The Building Blocks of Concurrency
- Channels: Communication Between Go-routines
- Select Statement: Managing Multiple Channels
- Synchronization Primitives
- Concurrency Patterns
- Context Package: Managing Cancellation and Timeouts.
- Best Practices and Common Pitfalls**
1.Introduction to Concurrency
Concurrency is the ability to handle multiple tasks simultaneously. In Go, concurrency is a first-class citizen, built into the language's core design. Go's approach to concurrency is based on Communicating Sequential Processes (CSP), a model that emphasizes communication between processes rather than shared memory.
2.Concurrency vs Parallelism:
Go-routines enable concurrency, which is the composition of independently executing processes.
Parallelism (simultaneous execution) may occur if the system has multiple CPU cores and the Go runtime schedules go-routines to run in parallel.
3. Go-routines:
The Building Blocks of Concurrency is Go-routines are lightweight threads managed by the Go runtime. It's a function or method that runs concurrently with other functions or methods. Go-routines are the foundation of Go's concurrency model.
Key Characteristics:
- Lightweight: Go-routines are much lighter than OS threads. You can easily create thousands of go-routines without significant performance impact.
- Managed by Go runtime: The Go scheduler handles the distribution of go-routines across available OS threads.
- Cheap creation: Starting a go-routine is as simple as using the go keyword before a function call.
- Stack size: Go-routines start with a small stack (around 2KB) that can grow and shrink as needed.
Creating a Go-routine:
To start a go-routine, you simply use the go keyword followed by a function call:
go functionName()
Or with an anonymous function:
go func() {
// function body
}()
Go-routine Scheduling:
- The Go runtime uses a M:N scheduler, where M go-routines are scheduled onto N OS threads.
- This scheduler is non-preemptive, meaning go-routines yield control when they are idle or logically blocked.
Communication and Synchronization:
- Goroutines typically communicate using channels, adhering to the "Don't communicate by sharing memory; share memory by communicating" principle.
- For simple synchronization, you can use primitives like
sync.WaitGroup
orsync.Mutex
.
Example with Explanation:
package main
import (
"fmt"
"time"
)
func printNumbers() {
for i := 1; i <= 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Printf("%d ", i)
}
}
func printLetters() {
for i := 'a'; i <= 'e'; i++ {
time.Sleep(150 * time.Millisecond)
fmt.Printf("%c ", i)
}
}
func main() {
go printNumbers()
go printLetters()
time.Sleep(2 * time.Second)
fmt.Println("\nMain function finished")
}
Explanation:
- We define two functions: printNumbers and printLetters.
- In main, we start these functions as goroutines using the go keyword.
- The main function then sleeps for 2 seconds to allow the goroutines to complete.
- Without goroutines, these functions would run sequentially. With goroutines, they run concurrently.
- The output will show numbers and letters interleaved, demonstrating concurrent execution.
Goroutine Lifecycle:
- A goroutine starts when created with the go keyword.
- It terminates when its function completes or when the program exits.
- Goroutines can be leaked if not properly managed, so it's important to ensure they can exit.
Best Practices:
- Don't create goroutines in libraries; let the caller control concurrency.
- Be cautious about creating an unbounded number of goroutines.
- Use channels or sync primitives to coordinate between goroutines.
- Consider using worker pools for managing multiple goroutines efficiently.
Simple example with explanations of go-routines
package main
import (
"fmt"
"time"
)
// printNumbers is a function that prints numbers from 1 to 5
// It will be run as a goroutine
func printNumbers() {
for i := 1; i <= 5; i++ {
time.Sleep(500 * time.Millisecond) // Sleep for 500ms to simulate work
fmt.Printf("%d ", i)
}
}
// printLetters is a function that prints letters from 'a' to 'e'
// It will also be run as a goroutine
func printLetters() {
for i := 'a'; i <= 'e'; i++ {
time.Sleep(300 * time.Millisecond) // Sleep for 300ms to simulate work
fmt.Printf("%c ", i)
}
}
func main() {
// Start printNumbers as a goroutine
// The 'go' keyword before the function call creates a new goroutine
go printNumbers()
// Start printLetters as another goroutine
go printLetters()
// Sleep for 3 seconds to allow goroutines to finish
// This is a simple way to wait, but not ideal for production code
time.Sleep(3 * time.Second)
// Print a newline for better formatting
fmt.Println("\nMain function finished")
}
4.Channels :
Channels are a core feature in Go that allow go-routines to communicate with each other and synchronize their execution. They provide a way for one go-routine to send data to another go-routine.
Purpose of Channels
Channels in Go serve two main purposes:
a) Communication: They allow goroutines to send and receive values to and from each other.
b) Synchronization: They can be used to synchronize execution across goroutines.
Creation: Channels are created using the make function:
ch := make(chan int) // Unbuffered channel of integers
Sending: Values are sent to a channel using the <- operator:
ch <- 42 // Send the value 42 to the channel
Receiving: Values are received from a channel using the <- operator:
value := <-ch // Receive a value from the channel
Types of Channels
a) Unbuffered Channels:
- Created without a capacity: ch := make(chan int)
- Sending blocks until another goroutine receives.
- Receiving blocks until another goroutine sends.
ch := make(chan int)
go func() {
ch <- 42 // This will block until the value is received
}()
value := <-ch // This will receive the value
b) Buffered Channels:
- Created with a capacity: ch := make(chan int, 3)
- Sending only blocks when the buffer is full.
- Receiving only blocks when the buffer is empty.
ch := make(chan int, 2)
ch <- 1 // Doesn't block
ch <- 2 // Doesn't block
ch <- 3 // This will block until a value is received
Channel Directions
Channels can be directional or bidirectional:
- Bidirectional: chan T
- Send-only: chan<- T
- Receive-only: <-chan T
Example :
func send(ch chan<- int) {
ch <- 42
}
func receive(ch <-chan int) {
value := <-ch
fmt.Println(value)
}
Closing Channels
Channels can be closed to signal that no more values will be sent:
close(ch)
Receiving from a closed channel:
If the channel is empty, it returns the zero value of the channel's type.
You can check if a channel is closed using a two-value receive:
value, ok := <-ch
if !ok {
fmt.Println("Channel is closed")
}
Ranging over Channels
You can use a for range loop to receive values from a channel until it's closed:
for value := range ch {
fmt.Println(value)
}
Hey, Thank you for staying until the end! I appreciate you being valuable reader and learner. Please follow me here and also on my Linkedin and GitHub .
Top comments (1)
zerogpt.com/ 60% AI written