Understanding Concurrency and Parallelism in Programming with Go

Photo by Scott Webb on Unsplash

Understanding Concurrency and Parallelism in Programming with Go

In the world of programming, the terms "concurrency" and "parallelism" often come up, especially when discussing performance and efficiency. But what do these terms really mean? In this post, we'll explore these concepts in a simple, easy-to-understand manner, and then dive into some real coding examples using Go (Golang), a language renowned for its robust support for concurrency.

Concurrency Explained with an Analogy

Concurrency is about dealing with multiple tasks at once. Let's use a kitchen analogy: imagine a chef who needs to boil pasta, grill chicken, and chop vegetables. The chef doesn't cook everything at the exact same moment; rather, they manage these tasks in an overlapping manner - maybe stirring the pasta, then chopping vegetables while waiting for the pasta to boil.

In programming, concurrency is similar. Your program is set up to handle multiple tasks, which might be in progress or executed in overlapping time frames, but not necessarily simultaneously.

Parallelism Explained with an Analogy

Parallelism, on the other hand, is about doing multiple tasks at the same time. In our kitchen, if the chef had a helper, they could grill chicken while the helper chops vegetables - both tasks happening simultaneously.

In the context of programming and multi-core processors, parallelism occurs when different cores process different tasks concurrently, akin to having multiple chefs (cores) working simultaneously.

Concurrency in Go: A Coding Example

To illustrate concurrency, let's look at a Go example where we have two tasks: sending emails and logging data. We want these tasks to run independently but not necessarily at the same time.

package main

import (
    "fmt"
    "time"
)

func sendEmails() {
    for i := 0; i < 5; i++ {
        fmt.Println("Sending email", i)
        time.Sleep(1 * time.Second) 
    }
}

func logData() {
    for i := 0; i < 5; i++ {
        fmt.Println("Logging data", i)
        time.Sleep(2 * time.Second) 
    }
}

func main() {
    go sendEmails()
    go logData()

    time.Sleep(10 * time.Second)
    fmt.Println("Main function complete")
}

Here, sendEmails and logData are running concurrently. They are managed by Go's runtime and can be executed in an overlapping manner.

Parallelism in Go: A Coding Example

Let's now demonstrate parallelism. Suppose we have a task of calculating Fibonacci numbers, which is computation-intensive.

package main

import (
    "fmt"
    "runtime"
)

func calculateFib(n int) {
    a, b := 0, 1
    for i := 0; i < n; i++ {
        a, b = b, a+b
    }
    fmt.Printf("Fib(%d): %d\n", n, a)
}

func main() {
    runtime.GOMAXPROCS(runtime.NumCPU()) 

    go calculateFib(40)
    go calculateFib(45)

    fmt.Scanln()
}

By running the Fibonacci calculations as separate goroutines and utilizing all available CPU cores, these functions can run in parallel, potentially on different cores.

Go (Golang) is designed to support both concurrency and parallelism, but how your Go program runs - concurrently or in parallel - depends on a few factors:

  1. Concurrency in Go:

    • Inherent Support: Go inherently supports concurrency through goroutines and channels. Goroutines are lightweight threads managed by the Go runtime, allowing you to run multiple tasks concurrently in an efficient and straightforward way.

    • Use Case: Concurrency in Go is used to structure your program to handle multiple tasks that can be carried out independently. This is more about the organization and management of tasks rather than their simultaneous execution.

  2. Parallelism in Go:

    • Depends on Hardware: Parallelism in Go depends on the hardware it's running on, specifically the number of CPU cores available. If your machine has multiple cores, then Go can execute multiple goroutines in parallel, each on a different core.

    • Configurable: You can control the level of parallelism in your Go program using runtime.GOMAXPROCS. This setting determines the number of system threads that can execute Go code simultaneously. By default, it is set to the number of available CPU cores.

  3. Execution Environment:

    • Single-Core CPU: If your program is running on a single-core CPU, goroutines will not run in parallel but will still be concurrent. The Go runtime will efficiently manage goroutines, scheduling them to run in turns on the single core.

    • Multi-Core CPU: On a multi-core CPU, goroutines can run both concurrently and in parallel. Different goroutines can be executed at the same time on different cores.

In summary, Go is designed to be good at both concurrency and parallelism. The concurrency features (like goroutines and channels) are a part of the language's core, making it easy to write programs that handle multiple tasks. Whether these tasks run in parallel depends on the underlying hardware and how you've configured the runtime settings.