Computing Everything: The Chaos of Concurrency

Hi, Aviral this side — a young hobbyist software developer and a tinkerer from India. With a keen interest in Backend systems, Distributed/Cloud computing, Security, DevOps and Electronics - I enjoy hunting for bugs and challenging engineering problems. Code, keyboard's tak-tak(s), and coffee are my constant companions.
Introduction
(For best experience read this writeup on my blog - https://mraviral.in/blog/concurrency-chaos )
At its core, computing is linear. A CPU is a simple creature: it reads an instruction, executes it, and moves to the next. It does exactly one thing at a time, faithfully and sequentially.
But the world isn't linear. Web servers need to handle thousands of requests simultaneously; your OS plays music while you compile code; games render 4K graphics while calculating complex physics.
Concurrency is the art of cheating linearity.
It is the sleight of hand that makes a single processor appear to be in five places at once. It is the engineering of chaos into order. But this magic comes with a steep price. When you step out of the safe, sequential world of "do A, then B," you enter a realm where time is fluid, shared memory is a battlefield, and bugs don't just crash your program—they lurk in the shadows, waiting for the perfect nanosecond to strike.
Welcome to the difficult, dangerous, and essential world of concurrency.
Imagine you are a chef in a busy kitchen. You have to chop onions, boil pasta, and toast a bread. If you do these things one by one—chop all onions, then wait for water to boil, then toast the bread—the dinner will be late and cold.
Instead, you switch tasks. You put the water on the stove. While it heats, you chop onions. You pause chopping to drop the pasta in. You flip the bread.
This is concurrency. It is the art of structuring a program so that it deals with multiple things at once, rather than doing them one after another.
The Golden Rule: Concurrency is about structure (handling many tasks). Parallelism is about execution (doing many tasks simultaneously). You can have concurrency on a single-core CPU by switching tasks fast enough to create the illusion of simultaneity.
1. The Actors: Processes vs. Threads
To compute things concurrently, we need containers for our code.
The Process
Think of a Process as a house. It has its own address (memory space), its own resources, and it is isolated from other houses. If a process crashes, it usually doesn't burn down the neighbor's house. However, communicating between houses (Inter-Process Communication) is slow because you have to walk outside to talk.
The Thread
Think of Threads as the people living inside the house. They share the same kitchen, the same bathroom, and the same air (shared memory). Threads are "lightweight." Spinning up a thread is faster than building a house.
The Danger: Because threads share memory, if one thread leaves a mess in the kitchen (corrupts data), all other threads suffer.
The Go Twist: Goroutines
Go uses Goroutines. These are not OS threads; they are "Green Threads." The Go runtime manages them. They are incredibly cheap (kilobytes of memory vs. megabytes for OS threads). You can spin up hundreds of thousands of them.
package main
import (
"fmt"
"time"
)
func sayHello() {
for i := 0; i < 5; i++ {
fmt.Println("Hello from Goroutine!")
time.Sleep(100 * time.Millisecond)
}
}
func main() {
// The 'go' keyword spins off a concurrent task
go sayHello()
// The main function continues running its own tasks
for i := 0; i < 3; i++ {
fmt.Println("Hello from Main!")
time.Sleep(100 * time.Millisecond)
}
// Note: If main finishes, the program exits, killing the goroutine.
}
2. The Choreography: Switching and Event Loops
Context Switching
How does a single CPU core run 50 threads? It cheats. It runs Thread A for a few milliseconds, pauses it, saves its state (registers, stack pointer), loads the state of Thread B, runs it, and repeats. This is Context Switching. It is expensive overhead—like putting away your chopping board to get out the frying pan every 30 seconds.
The Event Loop
Some systems (like Node.js or UI threads) reject the "many threads" model. They use an Event Loop. Imagine a single waiter in a restaurant. He doesn't cook the food. He takes an order, gives it to the kitchen (system I/O), and immediately goes to the next table. When the kitchen says "Order Up!" (Event), he serves it.
Pros: No complex locking needed (mostly).
Cons: If the waiter does complex math (CPU heavy task), the whole restaurant halts.
3. Sharing Data: The Root of All Evil
When threads share memory, we encounter Race Conditions. Imagine two people trying to withdraw $100 from a shared bank account with $150 in it at the exact same time. Both check the balance ($150), both think it's okay, both withdraw. The bank loses $50.
The Solution: Locks (Mutex)
A Mutex (Mutual Exclusion) is a lock on the bathroom door. Only one thread can hold the lock. Everyone else must line up and wait.
package main
import (
"fmt"
"sync"
)
var (
counter int
mu sync.Mutex // The Lock
)
func increment(wg *sync.WaitGroup) {
defer wg.Done()
// LOCK: "I am using the counter now"
mu.Lock()
// Critical Section: Only one goroutine can be here at a time
counter++
// UNLOCK: "I am done"
mu.Unlock()
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go increment(&wg)
}
wg.Wait()
fmt.Println("Final Counter:", counter) // Always 1000. Without lock, it would be random.
}
Semaphores
A Mutex allows 1 person in. A Semaphore allows $N$ people in. Think of it like a bouncer at a club who only lets 5 people inside at a time. In Go, we often use Buffered Channels as semaphores.
// A semaphore allowing 3 concurrent tasks
sem := make(chan struct{}, 3)
for i := 0; i < 10; i++ {
go func() {
sem <- struct{}{} // Acquire token (blocks if full)
// ... perform expensive work ...
<-sem // Release token
}()
}
4. The Nightmares: Deadlock, Livelock, Starvation
Concurrency introduces bugs that disappear when you look for them (Heisenbugs).
Deadlock
Two threads stop forever because they are waiting for each other.
Thread A holds Lock 1 and waits for Lock 2.
Thread B holds Lock 2 and waits for Lock 1.
Result: The program hangs eternally.
Livelock
Similar to deadlock, but the threads are active. Imagine two people walking down a hallway. They meet face to face. Both step left. Both step right. Both step left. They are moving (using CPU), but making no progress.
Starvation
A greedy thread constantly acquires the lock, or the scheduler constantly gives it priority. The "polite" thread waits forever and never gets to run.
5. Coordination: The Way Out
Lock Ordering
To prevent deadlocks, establish a strict hierarchy. If you need Lock A and Lock B, you must always acquire Lock A before Lock B. If everyone follows this rule, a cycle (deadlock) is impossible.
Lock-Free Programming (Atomic)
Locks put threads to sleep. This is slow. Lock-free programming uses hardware instructions (like CAS - Compare And Swap) to update data safely without stopping.
Warning: This is extremely hard to get right.
In Go, use the
sync/atomicpackage.
import "sync/atomic"
var ops uint64
func main() {
// Atomically add 1 to ops. No mutex required.
// Ideally suited for simple counters.
atomic.AddUint64(&ops, 1)
}
Message Passing (Channels)
This is the "Go philosophy."
"Don't communicate by sharing memory; share memory by communicating."
Instead of locking a variable, send the data from Thread A to Thread B through a pipe. Thread A doesn't touch it anymore. Thread B owns it now. No locks needed.
package main
import "fmt"
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
fmt.Printf("Worker %d processing job %d\n", id, j)
results <- j * 2 // Send result back
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
// Spin up 3 workers (The Thread Pool)
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// Send 5 jobs
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs) // Close the channel to signal no more work
// Collect results
for a := 1; a <= 5; a++ {
<-results
}
}
Conclusion: The Double-Edged Sword
Concurrency is the most powerful tool in a developer's arsenal, but it is also the most dangerous. Used correctly, it unlocks the full potential of modern hardware, turning sluggish applications into high-performance machines. Used carelessly, it creates a minefield of race conditions and deadlocks that no debugger can easily untangle.
Mastering this requires more than just knowing syntax - You must stop thinking in straight lines and start thinking in independent, coordinating units.



