DEV Community

Cover image for Understanding Parallelism vs Concurrency in Go
BHARGAB KALITA
BHARGAB KALITA

Posted on

Understanding Parallelism vs Concurrency in Go

When we talk about parallelism and concurrency, it's easy to confuse the two because they both involve executing multiple tasks at the same time. However, they are fundamentally different concepts with distinct applications, especially in Go, a language designed with these features at its core. Let's break down what each means, their differences, and how we can achieve them in Go.

Concurrency: Dealing with Lots of Tasks

Concurrency is the ability to handle multiple tasks at once, even if they aren't being executed simultaneously. Think of it as juggling several tasks, but not necessarily completing them at the same time.

Imagine you’re working on a report while also replying to emails. You aren't writing the report and typing emails simultaneously, but you’re switching back and forth between the two. This switching allows both tasks to make progress without waiting for one to be completed before starting the other. Similarly, concurrency in computing lets a system handle multiple operations or tasks by switching between them, giving the illusion of simultaneous execution.

In Go, concurrency is achieved using goroutines. A goroutine is a lightweight thread of execution, allowing Go to run functions concurrently. For example, you can have multiple goroutines that make network calls, process data, or run other functions at the same time.

Here's a simple example of concurrency in Go:

package main

import (
    "fmt"
    "time"
)

func printNumbers() {
    for i := 1; i <= 5; i++ {
        fmt.Println(i)
        time.Sleep(100 * time.Millisecond)
    }
}

func printLetters() {
    for i := 'A'; i <= 'E'; i++ {
        fmt.Printf("%c\n", i)
        time.Sleep(100 * time.Millisecond)
    }
}

func main() {
    go printNumbers() // Run printNumbers concurrently
    go printLetters() // Run printLetters concurrently

    time.Sleep(1 * time.Second) // Wait to ensure both goroutines finish
}
Enter fullscreen mode Exit fullscreen mode

In this example, printNumbers and printLetters are run as goroutines, meaning they execute concurrently. The system alternates between running them, resulting in interleaved output, even though the two functions aren’t running at the exact same time.

Parallelism: Doing Multiple Things at Once

Parallelism, on the other hand, is about actually performing multiple tasks simultaneously. This only occurs when you have multiple processors or cores. Imagine you and a friend are both typing two different documents at the same time. That's parallelism—two tasks happening at the same time on different CPUs or cores.

Parallelism is a subset of concurrency. All parallelism is concurrency, but not all concurrency is parallelism. Parallelism requires hardware that supports simultaneous task execution, whereas concurrency is more about structuring tasks to improve responsiveness and throughput, regardless of whether they run in parallel.

In Go, parallelism is achieved when goroutines are distributed across multiple CPU cores. Go allows you to control the number of cores that can execute goroutines using the GOMAXPROCS setting. By default, Go will run goroutines across as many CPU cores as are available, making it easy to take advantage of parallelism when the hardware allows it.

Here’s an example of how you can use GOMAXPROCS to enable parallelism in Go:

package main

import (
    "fmt"
    "runtime"
    "sync"
)

func task(id int, wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Printf("Task %d is running\n", id)
}

func main() {
    runtime.GOMAXPROCS(4) // Set the number of OS threads (cores) to use

    var wg sync.WaitGroup
    for i := 1; i <= 10; i++ {
        wg.Add(1)
        go task(i, &wg) // Run tasks concurrently and in parallel (if available)
    }
    wg.Wait() // Wait for all tasks to complete
}
Enter fullscreen mode Exit fullscreen mode

In this case, the GOMAXPROCS function sets the number of CPU cores available to run goroutines in parallel. With 4 cores, Go can execute up to 4 tasks simultaneously, fully utilizing the available hardware.

Key Differences Between Concurrency and Parallelism

  • Concurrency is about dealing with multiple tasks at the same time, but not necessarily running them at the same instant. It’s more about structuring the program to make progress on many tasks by rapidly switching between them.
  • Parallelism is about executing multiple tasks simultaneously on different processors or cores.

Think of concurrency as juggling many balls (tasks) by switching focus between them, while parallelism is juggling many balls by using multiple hands (cores).

Achieving Concurrency and Parallelism in Go

Go makes both concurrency and parallelism simple to implement, thanks to its goroutine model and built-in support for multi-core systems.

  • Concurrency is achieved by using goroutines to run tasks independently without waiting for each to complete.
  • Parallelism happens when these goroutines are distributed across multiple CPU cores, which Go handles automatically but can be fine-tuned using GOMAXPROCS.

Go's design philosophy emphasizes making concurrency easy to work with through its channels and goroutines, and it automatically optimizes for parallelism when the underlying hardware supports it.

Real-Life Examples of Concurrency and Parallelism

  1. Web Server (Concurrency)

    Imagine running an e-commerce website like Amazon. Multiple users are browsing, placing orders, and checking out simultaneously. The web server handles each request concurrently, ensuring that no single user is blocked by another. The server can switch between handling requests for displaying product pages, adding items to the cart, or processing payment, without waiting for each to complete sequentially.

  2. Video Streaming Platform (Parallelism)

    In a video-sharing platform like YouTube, users upload videos that need to be transcoded into multiple formats (1080p, 720p, etc.). Instead of processing each video sequentially, parallelism allows the platform to transcode several videos at the same time using multiple CPU cores. This speeds up processing, allowing quicker availability of videos for streaming.

  3. Chat Application (Concurrency)

    A real-time chat application like WhatsApp needs to handle messages from many users concurrently. Each user sends and receives messages independently, so the system processes each user’s messages concurrently, ensuring a smooth real-time experience for everyone without messages being delayed or blocked by others.

  4. Machine Learning (Parallelism)

    In machine learning, training models on large datasets can take hours. By splitting the dataset into chunks, a system can use parallelism to train on multiple data chunks simultaneously across different CPU or GPU cores. This speeds up the training process, enabling faster model development for tasks like image recognition or recommendation engines.

  5. Traffic Management System (Concurrency)

    In a city traffic management system where cameras at multiple intersections send updates to a central server, concurrency is used to process real-time data from multiple locations at once. The system can concurrently handle traffic signal data, vehicle counts, and accident reports, ensuring smooth city-wide traffic flow without delay in processing data from different intersections.

Conclusion

In Go, concurrency is about managing multiple tasks and making progress on them, while parallelism takes that one step further by executing them simultaneously. Both are crucial for building efficient, scalable programs, and Go makes it incredibly easy to implement them with minimal overhead. Understanding the distinction allows developers to choose the right approach based on the problem they are solving—whether it’s to improve the responsiveness of an application with concurrency or to fully leverage CPU power with parallelism.

Playground: https://codesandbox.io/p/devbox/2qg64j

Top comments (0)