Concurrent Design Patterns
Introduction to Concurrent Design Patterns
Concurrent design patterns help manage complexity and improve the reliability of concurrent applications in Golang.
Common Concurrent Design Patterns
1. Worker Pool Pattern
type Job struct {
ID int
}
func workerPool(jobs <-chan Job, results chan<- int, workerCount int) {
var wg sync.WaitGroup
for i := 0; i < workerCount; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for job := range jobs {
result := processJob(job)
results <- result
}
}()
}
wg.Wait()
close(results)
}
2. Fan-Out/Fan-In Pattern
graph TD
A[Input Channel] --> B[Distributor]
B --> C1[Worker 1]
B --> C2[Worker 2]
B --> C3[Worker 3]
C1 --> D[Aggregator]
C2 --> D
C3 --> D
D --> E[Result Channel]
func fanOutFanIn(input <-chan int, workerCount int) <-chan int {
results := make(chan int)
var wg sync.WaitGroup
for i := 0; i < workerCount; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for num := range input {
results <- processNumber(num)
}
}()
}
go func() {
wg.Wait()
close(results)
}()
return results
}
3. Semaphore Pattern
type Semaphore struct {
semaChan chan struct{}
}
func NewSemaphore(maxConcurrency int) *Semaphore {
return &Semaphore{
semaChan: make(chan struct{}, maxConcurrency),
}
}
func (s *Semaphore) Acquire() {
s.semaChan <- struct{}{}
}
func (s *Semaphore) Release() {
<-s.semaChan
}
Synchronization Patterns
Mutex and RWMutex
Pattern |
Use Case |
Characteristics |
Mutex |
Exclusive access |
Blocks all access |
RWMutex |
Multiple readers |
Allows concurrent reads |
type SafeCounter struct {
mu sync.RWMutex
counters map[string]int
}
func (c *SafeCounter) Inc(key string) {
c.mu.Lock()
defer c.mu.Unlock()
c.counters[key]++
}
func (c *SafeCounter) Value(key string) int {
c.mu.RLock()
defer c.mu.RUnlock()
return c.counters[key]
}
Advanced Concurrency Patterns
1. Context Pattern
func fetchData(ctx context.Context, url string) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
2. Pipeline Pattern
func pipeline() <-chan int {
out := make(chan int)
go func() {
defer close(out)
for i := 1; i <= 10; i++ {
out <- i
}
}()
return out
}
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for num := range in {
out <- num * num
}
}()
return out
}
Best Practices
- Use channels for communication
- Minimize shared state
- Design for cancellation and timeouts
- Use context for managing concurrent operations
- Choose appropriate concurrency patterns
- Avoid over-synchronization
- Use buffered channels judiciously
- Profile and measure performance
Learning with LabEx
At LabEx, we provide comprehensive environments to explore and master Golang concurrent design patterns, helping developers build robust and efficient concurrent applications.