Concurrency vs. Parallelism in GoLang
- Concurrency: GoLang allows you to execute multiple tasks apparently simultaneously. This is achieved through goroutines, which are lightweight functions that execute concurrently. The Go runtime is responsible for scheduling these goroutines on the available system threads, giving the illusion of parallelism.
- Parallelism: Parallelism involves executing multiple tasks truly simultaneously, utilizing multiple processor cores. If you have a task that can be divided into independent subtasks, Go can leverage parallelism to accelerate execution.
Do goroutines run one by one or in parallel?
The short answer is: it depends.
- One by one: If there are not enough resources (CPU cores, memory) or if the tasks are tightly coupled, goroutines will execute sequentially, one after another.
- In parallel: If you have multiple cores available and the tasks are independent, the Go runtime will attempt to execute the goroutines in parallel, taking advantage of all available cores.
Is it possible to apply parallelism to save execution time?
Absolutely! GoLang is designed to facilitate concurrent and parallel programming. Here are some ways to take advantage of parallelism:
- Goroutines: Create multiple goroutines to execute independent tasks.
- Channels: Use channels to communicate and synchronize between goroutines.
- WaitGroups: Use
sync.WaitGroup
to wait for all goroutines to finish before proceeding. - Parallel data processing: Divide large datasets into smaller chunks and process each chunk in a different goroutine.
- Use parallelism libraries: Explore libraries like
parallel
that offer high-level functions for performing operations in parallel.
Simple example:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
// Code block for sequencial execution
t1 := time.Now()
sequencial()
t1f := time.Now()
t1t := t1f.Sub(t1)
fmt.Println("Sequencial took", t1t)
// Code block for routines. It cna apply parallelism due to free CPU
t2 := time.Now()
parallelism()
t2f := time.Now()
t2t := t2f.Sub(t2)
fmt.Println("Parallelism took", t2t)
}
func sequencial() {
for i := 0; i < 5; i++ {
fmt.Println("Execution", i)
waitTime()
}
}
func parallelism() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
fmt.Println("Execution", i)
go waitTimeParallel(&wg)
}
wg.Wait()
}
func waitTime() {
time.Sleep(5 * time.Second)
}
func waitTimeParallel(wg *sync.WaitGroup) {
time.Sleep(5 * time.Second)
wg.Done()
}
In this example, 5 goroutines are created that simulate independent tasks. By using sync.WaitGroup
, it is ensured that the main program waits for all goroutines to finish before terminating.
If we take a close look to the execution of the output, we might see how we can take advantage of parallelism and run each task in a different CPU core, so the total execution time will be drastically lower. Since the task is just a 5-seconds counter, it will run everything at the same time, counting only 5 seconds. In the sequencial example we see how it runs each task one after one, counting 5 seconds per each task.

Important considerations:
- Communication between goroutines: Channels are essential for coordinating work between goroutines.
- Synchronization: Use
sync.WaitGroup
, mutexes, or semaphores to synchronize access to shared resources. - Deadlocks: Beware of deadlocks, which can occur when two or more goroutines are waiting for another to perform an action.
- Overhead: Creating and managing many goroutines has a cost. Evaluate whether the benefit of parallelism justifies the overhead.
In summary:
GoLang provides you with powerful tools for leveraging concurrency and parallelism. By understanding the basic concepts and applying them correctly, you can write more efficient and scalable programs.