DEV Community

adiozdaniel
adiozdaniel

Posted on

Diving Deep into Concurrency in Go (Without Channels)

Go’s concurrency model is one of its standout features. While channels often steal the spotlight for goroutine communication, understanding concurrency at its core, without relying on channels, is crucial for mastering Go. This post dives deep into goroutines, the sync package, and practical patterns for synchronization.

Concurrency vs. Parallelism

Concurrency is about managing multiple tasks at once, while parallelism is about executing multiple tasks simultaneously. Go is designed for concurrency, making it easy to structure programs that can handle multiple operations independently.


Goroutines: The Building Blocks

A goroutine is a lightweight thread managed by the Go runtime. Creating a goroutine is as simple as prefixing a function call with go.

package main  

import (  
    "fmt"  
    "time"  
)  

func task(name string) {  
    for i := 0; i < 5; i++ {  
        fmt.Printf("Task %s is running: %d\n", name, i)  
        time.Sleep(time.Millisecond * 500)  
    }  
}  

func main() {  
    go task("A")  
    go task("B")  

    // Let the main function wait for goroutines to finish  
    time.Sleep(time.Second * 3)  
    fmt.Println("Main function exiting")  
}  
Enter fullscreen mode Exit fullscreen mode

Synchronization Without Channels

Using sync.WaitGroup

sync.WaitGroup is a powerful tool to wait for multiple goroutines to complete their work.

package main  

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

func worker(id int, wg *sync.WaitGroup) {  
    defer wg.Done() // Decrement the counter when the goroutine completes  
    fmt.Printf("Worker %d starting\n", id)  
    time.Sleep(time.Second)  
    fmt.Printf("Worker %d done\n", id)  
}  

func main() {  
    var wg sync.WaitGroup  

    for i := 1; i <= 3; i++ {  
        wg.Add(1)  
        go worker(i, &wg)  
    }  

    wg.Wait() // Wait for all goroutines to finish  
    fmt.Println("All workers completed")  
}  
Enter fullscreen mode Exit fullscreen mode

Using sync.Mutex

When multiple goroutines access shared data, race conditions can occur. A sync.Mutex ensures that only one goroutine can access a critical section of code at a time.

package main  

import (  
    "fmt"  
    "sync"  
)  

type Counter struct {  
    value int  
    mu    sync.Mutex  
}  

func (c *Counter) Increment() {  
    c.mu.Lock()  
    c.value++  
    c.mu.Unlock()  
}  

func main() {  
    counter := &Counter{}  
    var wg sync.WaitGroup  

    for i := 0; i < 10; i++ {  
        wg.Add(1)  
        go func() {  
            defer wg.Done()  
            counter.Increment()  
        }()  
    }  

    wg.Wait()  
    fmt.Printf("Final Counter Value: %d\n", counter.value)  
}  
Enter fullscreen mode Exit fullscreen mode

Practical Patterns

Worker Pool Without Channels

A worker pool is a pattern where multiple workers perform tasks concurrently. Instead of channels, workers can access tasks from a shared slice protected by a mutex.

package main  

import (  
    "fmt"  
    "sync"  
)  

func worker(id int, tasks *[]int, mu *sync.Mutex, wg *sync.WaitGroup) {  
    defer wg.Done()  

    for {  
        mu.Lock()  
        if len(*tasks) == 0 {  
            mu.Unlock()  
            return  
        }  
        task := (*tasks)[0]  
        *tasks = (*tasks)[1:]  
        mu.Unlock()  

        fmt.Printf("Worker %d processing task %d\n", id, task)  
    }  
}  

func main() {  
    tasks := []int{1, 2, 3, 4, 5}  
    var mu sync.Mutex  
    var wg sync.WaitGroup  

    for i := 1; i <= 3; i++ {  
        wg.Add(1)  
        go worker(i, &tasks, &mu, &wg)  
    }  

    wg.Wait()  
    fmt.Println("All tasks processed")  
}  
Enter fullscreen mode Exit fullscreen mode

Concurrency Pitfalls

  1. Race Conditions: When multiple goroutines access and modify shared data, race conditions can lead to unpredictable behavior. Tools like sync.Mutex and sync/atomic help mitigate this.
  2. Deadlocks: Occurs when goroutines wait indefinitely for resources locked by each other. Careful planning is essential to avoid deadlocks.
  3. Starvation: A goroutine may be blocked indefinitely if other goroutines dominate resources.

Conclusion

Concurrency in Go is incredibly powerful, even without channels. By mastering goroutines, the sync package, and common patterns, you can write efficient, high-performance programs. Experiment with these tools, and you’ll quickly see why Go is a go-to choice for concurrent programming!

Top comments (0)