Golang - Concurrency - What is Concurrency
The Core Idea: Doing Multiple Things at Once
Concurrency means dealing with multiple tasks at the same time. It doesn’t necessarily mean they are executing at the same instant, but rather that their execution is interleaved, allowing you to make progress on all of them.
Analogy: A Chef in the Kitchen
Imagine a chef cooking a meal with multiple dishes:
- Put a pot of water on the stove to boil for pasta.
- While the water is heating up, start chopping vegetables for a salad.
- Once the vegetables are chopped, check if the water is boiling.
- If it is, add the pasta. While the pasta cooks, prepare the salad dressing.
The chef is a single person (like a single CPU core) managing multiple tasks. They switch between tasks intelligently instead of doing them strictly one after another. This is concurrency.
Why is Concurrency Important in Go?
Modern computers have CPUs with multiple cores. To make your software fast and efficient, you need to take advantage of all that processing power. Go was designed from the ground up with concurrency as a first-class citizen, making it easier than in many other languages.
Key Benefits:
- Performance: Utilize multi-core processors to run tasks in parallel.
- Responsiveness: Keep applications, like web servers, responsive by handling multiple user requests simultaneously.
- Efficiency: Avoid wasting time waiting for slow operations (like network requests or reading files) to complete.
Go’s Concurrency Building Blocks
Go provides two main tools for building concurrent programs: Goroutines and Channels.
1. Goroutines: The “Workers”
A goroutine is an incredibly lightweight “thread” managed by the Go runtime, not the operating system.
- Lightweight: You can have thousands, or even millions, of goroutines running at once without crashing your system. A goroutine starts with only a few kilobytes of stack space, which can grow or shrink as needed.
- Easy to Start: Just add the
go
keyword before a function call to run it in a new goroutine.
Example: Starting a Goroutine
package main
import (
"fmt"
"time"
)
func sayHello() {
fmt.Println("Hello from the goroutine!")
}
func main() {
go sayHello() // Start a new goroutine
fmt.Println("Hello from the main function!")
// Wait for a moment to let the goroutine run
time.Sleep(1 * time.Second)
}
Note: We add
time.Sleep
here because themain
function would otherwise exit before thesayHello
goroutine gets a chance to run. In real-world code, we use better ways to manage this, likesync.WaitGroup
.
2. Channels: The “Communication Lines”
Channels are pipes that connect concurrent goroutines, allowing them to communicate and synchronize safely. You can send and receive values through channels.
- Type-Safe: Each channel is typed, meaning it can only transport values of a specific type (e.g.,
chan int
,chan string
). - Synchronization: Sending or receiving data on a channel is a blocking operation. This is a key feature! It guarantees that the sender and receiver are synchronized, preventing race conditions.
3. sync
Package: The “Rulebook”
For situations where you need more traditional locking mechanisms, Go’s sync
package provides tools like Mutex
and WaitGroup
.
-
sync.Mutex
(Mutual Exclusion): A Mutex is like a key to a room. Only one goroutine can hold the key (lock the mutex) at a time, ensuring that only one goroutine can access a shared piece of data.When to use it: When multiple goroutines need to read and write to a shared variable (e.g., a counter).
var mu sync.Mutex var counter int func increment() { mu.Lock() // Lock the door counter++ mu.Unlock() // Unlock the door }
-
sync.WaitGroup
: A WaitGroup is like a checklist. It waits for a collection of goroutines to finish their tasks.Add(n)
: Addsn
tasks to the checklist.Done()
: Marks one task as complete.Wait()
: Blocks until all tasks on the checklist are complete.
When to use it: When you need to wait for several background tasks to finish before proceeding.
var wg sync.WaitGroup wg.Add(1) // Add one task to the list go func() { defer wg.Done() // Mark task as done when function exits fmt.Println("Task complete!") }() wg.Wait() // Wait for all tasks to be marked as done
By combining these simple but powerful tools, you can build complex and efficient concurrent applications in Go.