This content originally appeared on DEV Community and was authored by SK
Single-threaded code already brings headaches. Add a second thread, it's a graduation from a basic headache.
The fix? Mutexes: traffic cops for your threads and data.
Once you understand them, thread sync becomes second nature, language agnostic.
Working in both C++ and Go, I’ve run into all the usual chaos:
- Race conditions that sometimes swallow data
- Segfaults from threads trampling memory
- And the silent killer: deadlocks
That last one’s the worst, no crash, no error. Just a dead program, stuck in an eternal thread standoff.
But it all starts to click when you get the core idea behind a mutex.
The best part? Every language speaks mutex:
- Go →
sync.Mutex - C++ →
std::mutex - Python →
threading.Lock() - Java →
ReentrantLock
In this post, I’ll break down mutexes as a concept, show you how deadlocks happen, and leave you with enough intuition to handle threaded code in any language.
Learn once → apply everywhere.
Mutexes: Mutual Exclusion Lock
Threads introduce a whole new category of problems, especially in Go, where spawning thousands is practically free.
Now imagine two threads hitting the same data source at the exact same time. That’s chaos. Race conditions, data corruption, mystery bugs, things you don’t want to debug, let alone explain to your team.
Enter mutexes: the traffic cops between your threads and shared data.
Without a lock:
thread A ---> data source <--- thread B
With a lock (shared between both threads):
thread A [lock]---> data source <---[lock] thread B
The mutex’s job is simple: only one thread enters at a time.
If thread A owns the lock, thread B gets told: "Wait your turn."
Here’s a simple example of slice access with and without locks:
Without locks:
package main
import (
"fmt"
"time"
)
func main() {
var numbers []int
// Spin up 5 goroutines that all append to the same slice.
for i := 0; i < 5; i++ {
go func(n int) {
// No locking here,this will likely cause a data race
numbers = append(numbers, n)
fmt.Println("Appended", n, "→", numbers)
}(i)
}
// Give them a moment to run
time.Sleep(1 * time.Second)
}
With locks:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var (
numbers []int
mu sync.Mutex
)
for i := 0; i < 5; i++ {
go func(n int) {
mu.Lock() // acquire the lock
defer mu.Unlock() // ensure it’s released, even on panic
numbers = append(numbers, n)
fmt.Println("Appended", n, "→", numbers)
}(i)
}
time.Sleep(1 * time.Second)
}
Notice how we do:
mu.Lock()
defer mu.Unlock()
The defer guarantees that no matter how we exit that goroutine, normal return or panic, the lock will be released.
Once a goroutine touches shared data, lock it down. Trust me, future you will be grateful.
So, what exactly is a deadlock?
Deadlocks
Back to our traffic cop analogy:
thread A [lock]---> data source <---[lock] thread B
This works because one shared lock controls access. But what happens when we introduce two shared locks in the same lane?
thread A [lock]--[lock]-> data source <---[lock] thread B
Now you’ve got two traffic cops, and neither knows who’s in charge. Thread A gets stuck waiting on both, forever ping-ponging in confusion. That’s a classic deadlock.
The usual suspect? Same Nested locks, calling a function that acquires a lock from within another function that’s already holding it.
Here’s a real-world example:
func (m *ScheduledTask) Create(...) (task, error) {
m.mu.Lock() // LOCK 1
defer m.mu.Unlock() // UNLOCK 1 at the end
// ... setup task ...
if err := m.saveTasks(); err != nil { // LOCK 2 inside
return task{}, err
}
return t, nil
}
Now look inside saveTasks:
func (m *ScheduledTask) saveTasks() error {
m.mu.Lock() // LOCK 2 (again)
defer m.mu.Unlock()
data, err := json.MarshalIndent(m.tasks, "", " ")
if err != nil {
return err
}
return os.WriteFile(tasks, data, 0644)
}
Deadlock.
Why? Because Create() already holds the lock, and saveTasks() tries to acquire it again, before the first one is released. Go routines don’t complain, they just silently freeze. No crash, no stack trace, just a zombie thread eating resources.
And the main thread? Blissfully unaware. Keeps running while your program hangs in limbo.
If you’re serious about building real-world software, you need to understand synchronization.
The concepts apply across languages. Here's the C++ version:
std::lock_guard<std::mutex> lk(globalIPCData.mapMutex); // locking before access
UIelement& u = uiSet.get(entityId);
Learn this well.
Once you see mutexes as traffic cops with absolute authority, most thread issues just vanish.
I’ll be posting more deep dives on backend topics,JavaScript, Golang, C++, and low-level systems on Substack. Would love to have you there; come say hi:
More Content:
Guide To Deep Learning
How To Suck Less At Databases
This content originally appeared on DEV Community and was authored by SK
SK | Sciencx (2025-07-08T11:00:00+00:00) How To Synchronize threads In Go.. Retrieved from https://www.scien.cx/2025/07/08/how-to-synchronize-threads-in-go/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.